pax_global_header00006660000000000000000000000064140113516320014506gustar00rootroot0000000000000052 comment=2de59cffa8dda85318b21a3328da06c7a5515a39 jsonld.js-4.0.1/000077500000000000000000000000001401135163200134145ustar00rootroot00000000000000jsonld.js-4.0.1/.editorconfig000066400000000000000000000003731401135163200160740ustar00rootroot00000000000000# http://editorconfig.org root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.{js,json,jsonld,yaml,yml}] indent_style = space indent_size = 2 [*.idl] indent_style = space indent_size = 4 jsonld.js-4.0.1/.eslintrc.js000066400000000000000000000002211401135163200156460ustar00rootroot00000000000000module.exports = { env: { browser: true, commonjs: true, node: true }, extends: 'eslint-config-digitalbazaar', root: true }; jsonld.js-4.0.1/.github/000077500000000000000000000000001401135163200147545ustar00rootroot00000000000000jsonld.js-4.0.1/.github/workflows/000077500000000000000000000000001401135163200170115ustar00rootroot00000000000000jsonld.js-4.0.1/.github/workflows/main.yml000066400000000000000000000040561401135163200204650ustar00rootroot00000000000000name: Node.js CI on: [push] jobs: test-node: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [8.x, 10.x, 12.x, 14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install run: npm install - name: Fetch test suites run: npm run fetch-test-suites - name: Run test with Node.js ${{ matrix.node-version }} run: npm run test-node test-karma: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install run: npm install - name: Fetch test suites run: npm run fetch-test-suites - name: Run karma tests run: npm run test-karma lint: runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - name: Run eslint run: npm run lint coverage: needs: [test-node, test-karma] runs-on: ubuntu-latest timeout-minutes: 10 strategy: matrix: node-version: [14.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - name: Install run: npm install - name: Fetch test suites run: npm run fetch-test-suites - name: Generate coverage report run: npm run coverage-ci - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: file: ./coverage/lcov.info fail_ci_if_error: true jsonld.js-4.0.1/.gitignore000066400000000000000000000002711401135163200154040ustar00rootroot00000000000000*.sublime-project *.sublime-workspace *.sw[nop] *~ .DS_Store .cdtproject .classpath .cproject .project .settings TAGS coverage dist node_modules npm-debug.log tests/webidl/*-new v8.log jsonld.js-4.0.1/CHANGELOG.md000066400000000000000000000543271401135163200152400ustar00rootroot00000000000000# jsonld ChangeLog ## 4.0.1 - 2021-02-11 ### Changed - State which protected term is being redefined. - Switch to `core-js@3`. ## 4.0.0 - 2021-02-11 ### Removed - **BREAKING**: RDFa parser moved to `jsonld-rdfa` package, remove `xmldom` dependency. ## 3.3.1 - 2021-02-10 ### Fixed - Add `lru-cache` to packages run through babel for bundles. Fixes use of arrow functions. ## 3.3.0 - 2021-01-21 ### Changed - Update `rdf-canonize` to 2.0.1. - **NOTE**: The `rdf-canonize` update changes browser support and removes forge in favor of the WebCrypto `crypto.subtle` API. A WebCrypto polyfill *may* be required if a jsonld.js API is used that calls `rdf-canonize`. The polyfill is needed if older browsers are targeted or when not using a secure context on some modern browsers. Due to other concerns and the expected minimal impact of this change, it is happening in a minor release. Please provide feedback if this decision causes problems. - Node.js 6 is no longer tested due to development tool dependency updates and to avoid adding additional testing complexity. Node.js 6 will otherwise still be supported until the next major release. Please report any issues found. ## 3.2.0 - 2020-10-13 ### Fixed - Empty-property scoped context should not effect the outer context. Note that in situations where this fix is used there is now an extra clone of the active context which could potentially cause performance issues. ## 3.1.1 - 2020-05-22 ### Fixed - Fix XHR document loader Link header processing. ## 3.1.0 - 2020-04-15 ### Fixed - Support recusrive scoped contexts. - Various EARL report updates. - Fixed `prependBase` to start path with a '/' for a zero length path if there is an authority in base. ### Changed - Better support for using a processed context for `null` and caching `@import`. - Don't set `@base` in initial context and don't resolve a relative IRI when setting `@base` in a context, so that the document location can be kept separate from the context itself. - Use `package.json` `version` field for EARL reports. ## 3.0.1 - 2020-03-10 ### Fixed - Exclude `@type` from added values in Merge Node Maps step 2.2.1. ## 3.0.0 - 2020-03-10 ### Notes - This release adds support for a majority of JSON-LD 1.1. Significant thanks goes to Gregg Kellogg! - **BREAKING**: A notable change is that the framing `omitGraph` default changed to match the JSON-LD 1.1 Framing spec. This is likely to cause issues in most current uses of `frame()`. Result handling similar to `framed['@graph'][0]` will have to be changed. Check your code. - The spec calls for various situations to issue warnings. This is currently done with `console.warn`. This will be replaced by a new event notification API in an upcoming release. ### Fixed - More support for `"@type": "@none"`. - JSON literal value handling issues (`null` and `[]`). - Always pass `typeScopedContext` to `_expandObject`. - Allow a keyword to exist when expanding in `_expandObject` when the key is `@included` or `@type`. - Improve `isDouble` to look for big integers. - URI `removeDotSegments` only ensures preceding '/' if was already absolute. - Do minimal checking to see if IRIs are valid by making sure they contain no whitespace. - Terms of the form of an IRI must map to the same IRI. - Terms of the form of a relative IRI may not be used as prefixes. - Match spec error code "invalid context entry" vs "invalid context member". - Keywords may not be used as prefixes. - Handle term definition on `@type` with empty map. - Handling of `@` values for `@reverse`. - Changes in object embedding. - Better support for graph framing. - Better frame validation. - Wildcard matching on `@id` and other `requireAll` semantics. - Default frame for lists. - Check unused scoped contexts for validity. ### Changed - Keep term definitions mapping to null so they may be protected. - **NOTE**: `LINK_HEADER_REL` in `lib/constants.js` has been deprecated and renamed to `LINK_HEADER_CONTEXT`. It remains for now but will be removed in a future release. - Changed framing defaults - `embed` to "@once" and warn on "@first" or "@last". - `pruneBlankNodeIdentifiers` based on processingMode. - `omitGraph` based on processingMode. - Replaced `removePreserve` with `cleanupPreserve` and `cleanupNulls`. - Remove unused framing `graphStack` code that was removed from the spec. ### Added - Support for `"@import"`. - Added support for `@included` blocks - Skip things that have the form of a keyword, with warning. - Support for expansion and compaction of values container `"@direction"`. - Support for RDF transformation of `@direction` when `rdfDirection` is 'i18n-datatype'. - Top level `@graph` omitted if `omitGraph` is `true`. - Check for invalid values of `@embed`. - Support default values for `@type` when framing. ## 2.0.2 - 2020-01-17 ### Fixed - More support for `"@type": "@none"`. - JSON literal value handling issues (`null` and `[]`). - Fix resolving context `null` values. ### Changed - `isKeyword()` optimization for non-keyword fast path. ## 2.0.1 - 2019-12-10 ### Fixed - JSON literal value handling issues. ## 2.0.0 - 2019-12-09 ### Notes - An important **BREAKING** change in this release is the removal of callback support in favor of Promises and async/await. This release does **not** include a backwards compatible layer. If you need callback support, please use the 1.x releases, the Node.js `callbackify` feature, or another similar utility library. Suggestions on how best to provide a backwards compatibility layer are welcome. ### Fixed - Expanding the value of a graph container which is already a graph object generates a recursive graph object. - Compacting multiple nodes in a graph container places them in `@included`. - Indexing on `@type` requires `@type` to be either `@id` or `@vocab`, and defaults to `@id`. - Expanding/compacting type scoped contexts uses context before applying new versions to look for type scopes. ### Changed - Default processing mode changed to json-ld-1.1. Allows a 1.1 context to be used after non-1.1 contexts. - Indexing on an arbitrary property, not just `@index`. - `@vocab` can be relative or a Compact IRI in 1.1, resolved against either a previous `@vocab`, `@base` or document base. - Better checking of absolute IRIs. - Terms that begin with a ':' are not considered absolute or compact IRIs. - Don't use terms with `"@prefix": false` or expanded term definitions to construct compact IRIs. - `@type` may be used as a term definition only if `"@container": "@set"`. - Improve support for term propagation. - Context propagation no longer strictly related to use for property-scoped or term-scoped contexts and can be overridden. - Refactored internal context resolution. Processed context cache feature added. To be documented later. ### Removed - **BREAKING**: Remove callback API support. This includes removing support for callback-based document loaders and RDF parsers. This is done to facilitate JSON-LD 1.1 document loader features and to remove deprecated code. - **BREAKING**: Remove deprecated `loadDocument` API and obsolete `DocumentCache`. - **BREAKING**: Remove deprecated support for parsing legacy dataset format. ## 1.8.1 - 2019-10-24 ### Fixed - Run babel on canonicalize. Fixes arrow function compatibility issue. ## 1.8.0 - 2019-09-10 ### Added - Support literal JSON. - **NOTE**: The JSON serialization is based on the JSON Canonicalization Scheme (JCS) drafts. Changes in the JCS algorithm could cause changes in the `toRdf` output. ## 1.7.0 - 2019-08-30 ### Added - Support list of lists. ## 1.6.2 - 2019-05-21 ### Fixed - Allow overriding of protected terms when redefining to the same definition, modulo the `protected` flag itself. - Fix type-scoped context application: - Ensure values and subject references are expanded and compacted using type-scoped contexts, if available. - Ensure `@type` values are evaluated against the previous context, not the type-scoped context. ## 1.6.1 - 2019-05-13 ### Fixed - Ensure `@type`-scoped terms are limited to their `@type`-scoped object. ## 1.6.0 - 2019-04-17 ### Fixed - Testing: Use explicit id and description skipping regexes. - Usage of JavaScript Object property names in data. - **NOTE**: A class of bugs was causing term names such as `toString`, `valueOf`, and others to be dropped or produce bogus output. The fix could cause output triples to differ from previous versions if those special names were used. - Specifically, the problem was using `x in obj` instead of `obj.hasOwnProperty(x)` or a `Map`. - Fixed usage in contexts for expand and compact. - Attempted fixes in other parts of the code with similar `x in obj` usage. Finding actual realistic failing test cases proved difficult. ### Changed - Testing: Improve skip logging. ### Added - Testing: `skip` and `only` flags in manifests. - Testing: `VERBOSE_SKIP=true` env var to debug skipping. - Support `@protected`. - Support experimental non-standard `protectedMode` option. ## 1.5.4 - 2019-02-28 ### Fixed - Handle ` ` triple. ## 1.5.3 - 2019-02-21 ### Fixed - Improve handling of canonize test cases. - Update rdf-canonize dependency to address N-Quads parsing bug. ## 1.5.2 - 2019-02-20 ### Changed - Switch to eslint. - Optimize ensuring value is an array. ## 1.5.1 - 2019-02-01 ### Fixed - Update canonize docs. ## 1.5.0 - 2019-01-24 ### Changed - [rdf-canonize][] updated: - **BREAKING**: A fix was applied that makes the canonical output format properly match the N-Triples canoncial format. This fixes the format to no longer escape tabs in literals. This may cause canonical output from `jsonld.normalize()`/`jsonld.canonize()` to differ from previous versions depending on your literal data. If a backwards compatibility mode is needed please use 1.4.x and file an issue. - **BREAKING**: [rdf-canonize-native][] was removed as an indirect optional dependency and the JavaScript implemenation is now the default. The former `usePureJavaScript` flag was removed and a new `useNative` flag was added to force use of the native bindings. Higher level applications must explicitly install `rdf-canonize-native` to use this mode. Note that in many cases the JavaScript implemenation will be *faster*. Apps should be benchmarked before using the specialized native mode. - **NOTE**: The Travis-CI C++11 compiler update fixes are no longer needed when using jsonld.js! [rdf-canonize-native][] was updated to not use C++11 features and is also no longer a direct or indirect dependency of jsonld.js. ### Fixed - `rdfn:Urgna2012EvalTest` and `rdfn:Urdna2015EvalTest` tests should compare with expected output. ## 1.4.0 - 2019-01-05 ### Changed - PhantomJS is deprecated, now using headless Chrome with Karma. - **NOTE**: Using headless Chrome vs PhantomJS may cause newer JS features to slip into releases without proper support for older runtimes and browsers. Please report such issues and they will be addressed. - Update webpack and babel. - Use CommonJS style in main file. - **NOTE**: This change *might* cause problems if code somehow was still using the long deprecated `jsonldjs` global. Any dependants on this feature should update to use bundler tools such as webpack or use `jsonld` in the distributed bundle. ## 1.3.0 - 2019-01-04 ### Fixed - Use rdf-canonize to compare n-quads test results. - Maintain multiple graphs. - Sort `@type` when looking for scoped contexts. ### Changed - Use JSON-LD WG tests. - Categorize and skip failing tests. ## 1.2.1 - 2018-12-11 ### Fixed - Fix source map generation. ## 1.2.0 - 2018-12-11 ### Notes - The updated [rdf-canonize][] extracted out native support into [rdf-canonize-native][] and now has an *optional* dependency on this new module. If you have build tools available it will still build and use native support otherwise it will fallback to less performant JS code. - If you wish to *require* the native `rdf-canonize` bindings, add a dependency in your code to `rdf-canonize-native` to insure it is installed. - Some systems such as [Travis CI](https://travis-ci.org) currently only have ancient compilers installed by default. Users of `rdf-canonize`, and hence `jsonld.js`, previously required special setup so the `rdf-canonize` native bindings would be installable. If CI testing is not performance critical you can now simplify your CI config, let those bindings fail to install, and use the JS fallback code. ### Changed - Update `rdf-canonize` dependency to 0.3. ### Added - Initial support for benchmarking. - Basic callback interface tests. - Add README note about running json-ld.org test suite. ### Removed - Callback version of every test. - Callback interface tests added to catch callback API errors. - Avoids duplication of running every test for promises and callbacks. - Simplifies testing code and makes async/await conversion easier. ## 1.1.0 - 2018-09-05 ### Added - Add `skipExpansion` flag to `toRdf` and `canonize`. ## 1.0.4 - 2018-08-17 ### Fixed - Fix `_findContextUrls` refactoring bug from 1.0.3. ## 1.0.3 - 2018-08-16 ### Changed - Improve performance of active context cache and find context urls: - Use Map/Set. - Cache initial contexts based on options. - Reduce lookups. - Update webpack/karma core-js usage: - Add Map, Set, and Array.from support. ## 1.0.2 - 2018-05-22 ### Fixed - Handle compactArrays option in `@graph` compaction. - Many eslint fixes. - Add missing await to createNodeMap() and merge(). ## 1.0.1 - 2018-03-01 ### Fixed - Don't always use arrays for `@graph`. Fixes 1.0 compatibility issue. ## 1.0.0 - 2018-02-28 ### Notes - **1.0.0**! - [Semantic Versioning](https://semver.org/) is now past the "initial development" 0.x.y stage (after 7+ years!). - [Conformance](README.md#conformance): - JSON-LD 1.0 + JSON-LD 1.0 errata - JSON-LD 1.1 drafts - Thanks to the JSON-LD and related communities and the many many people over the years who contributed ideas, code, bug reports, and support! ### Added - Expansion and Compaction using scoped contexts on property and `@type` terms. - Expansion and Compaction of nested properties. - Index graph containers using `@id` and `@index`, with `@set` variations. - Index node objects using `@id` and `@type`, with `@set` variations. - Framing default and named graphs in addition to merged graph. - Value patterns when framing, allowing a subset of values to appear in the output. ## 0.5.21 - 2018-02-22 ### Fixed - ES2018 features are being used. Update version check to use generated Node.js 6 code when using Node.js earlier than 8.6.0. ## 0.5.20 - 2018-02-10 ### Fixed - Typo handling legacy N-Quads dataset format. ## 0.5.19 - 2018-02-09 ### Fixed - Include String startsWith() compatibility code. ## 0.5.18 - 2018-01-26 ### Changed - Use the W3C standard MIME type for N-Quads of "application/n-quads". Accept "application/nquads" for compatibility. ### Fixed - Fix fromRdf with input triple having a nil subject. ## 0.5.17 - 2018-01-25 ### Changed - **BREAKING**: Release 0.5.x as latest branch. See the many many changes below since 0.4.x including many potential breaking changes. ## 0.5.16 - 2018-01-25 ### Removed - **BREAKING**: Remove `jsonld.version` API and `pkginfo` dependency. This feature added complexity and browser issues and the use case is likely handled by semantic versioning and using a proper dependency. ### Fixed - Do not use native types to create IRIs in value expansion. - Improved error detection for `@container` variations. - Handle empty and relative `@base`. - Remove shortcut from compactIri when IRI is a keyword (fixes compact-0073). ### Changed - Set processingMode from options or first encountered context. - Use array representation of `@container` in processing. - **BREAKING**: Check for keys in term definition outside that expected: `@container`, `@id`, `@language`, `@reverse`, and `@type`. This also sets up for additional keywords in 1.1. ## 0.5.15 - 2017-10-16 ### Changed - **BREAKING**: Use RDF JS (rdf.js.org) interfaces for internal representation of dataset and quads. This should only break code that was using undocumented internal data structures, backwards-compat code exists to handle external RDF parsers. - Update `rdf-canonize` to dependency with native support. ## 0.5.14 - 2017-10-11 ### Fixed - Allow empty lists to be compacted to any `@list` container term. Fixes compact-0074 test. ## 0.5.13 - 2017-10-05 ### Fixed - Remote context retrieval bug. ### Removed - **BREAKING**: Remove `promisify` API. ## 0.5.12 - 2017-10-05 ### Changed - **BREAKING**: Remove top-layer errors. ## 0.5.11 - 2017-09-28 ### Removed - **BREAKING**: Remove deprecated extensions API, including `jsonld.request`. ## 0.5.10 - 2017-09-21 ### Added - Add `expansionMap` and `compactionMap` options. These functions may be provided that will be called when an unmapped value or property will be dropped during expansion or compaction, respectively. The function map return either `undefined` to cause the default behavior, some other value to use instead of the default expanded/compacted value, or it may throw an error to stop expansion/compaction. ### Removed - **BREAKING**: Remove deprecated `objectify` and `prependBase` APIs. Now `objectify` can be achieved via the `@link` option in framing and `prependBase` can be found via `url.prependBase`. - **BREAKING**: Remove deprecated `namer` option from all public APIs, use `issuer` instead. - **BREAKING**: Last active context used is no longer returned as an optional parameter to the `compact` callback. - **BREAKING**: Do not expose deprecated `DocumentCache`. ### Changed - **BREAKING**: Change default canonicalization algorithm to `URDNA2015`. ## 0.5.9 - 2017-09-21 ### Fixed - Callbackify bugs. - Document loaders. - Request queue. - Handling of exceptions in callbacks. ### Added - Various toRDF tests. ### Changed - Move tests from test/ to tests/. ## 0.5.8 - 2017-09-20 ### Changed - Run all test-suite tests with promises and callbacks. ### Fixed - Use Node.js "global" or webpack polyfill. ## 0.5.7 - 2017-09-20 ### Fixed - Distribute all js files, for real this time. ## 0.5.6 - 2017-09-20 ### Fixed - Fix `toRDF()`. ## 0.5.5 - 2017-09-20 ### Fixed - Distribute all js files. ## 0.5.4 - 2017-09-20 ### Fixed - Generate all js files for Node.js 6. ## 0.5.3 - 2017-09-20 ### Changed - Significant code reorganization and splitting into multiple files. ### Removed - **BREAKING**: Explicit IE8 support. Webpack, babel, and/or polyfills may be of help if support is still needed. - **BREAKING**: jQuery document loader. Use the XHR loader. - `Object.keys` polyfill. Other tools can provide this. ### Fixed - Handling of "global". ## 0.5.2 - 2017-09-19 ### Fixed - Distribute browser files. ## 0.5.1 - 2017-09-19 ### Fixed - Distribute unminified bundle. ## 0.5.0 - 2017-09-18 ### Added - Add .editorconfig support. - `fetch-test-suites` and related `fetch-*-test-suite` NPM scripts. - Support for `@graph` `@container`. ### Removed - Bower support. Use NPM, a NPM proxy site, or build your own bundle. - Makefile. Use NPM script targets. ### Changed - Update url parser to remove default ports from URLs. - Skip spec version 1.1 tests. - **BREAKING**: Only support Node.js 6.x and later with ES2015 features. - Build and use custom Node.js 6.x output so async/await/etc can be used. - **BREAKING**: Move `js/jsonld.js` to `lib/jsonld.js`. - **BREAKING**: Switch to CommonJS. - **BREAKING**: Fixes to allow RFC3986 tests to pass. Some URI edge cases and certain base URIs with dot segments may cause different URI outputs. - Switch to Karma for browser testing. - Switch to webpack to build browser bundles. - Add explicit feature compatibility libs to browser bundles. - Use async APIs for test generation. - Done to allow testing in Node.js and browsers. - Required major testing changes to make everything async. - Workarounds added to get async generated mocha tests working. - Improved support for loading various types of tests. - Can load local files, test manifests, or plain js files (in Node.js). - Use ES2015 in tests and babel/webpack to support older platforms. - Use rdf-canonize library, remove local implementation. ## 0.4.12 - 2017-04-24 ### Fixed - Fix `promises.compact` API when called with only 2 parameters. ## 0.4.11 - 2016-04-24 ### Changed - Add optimization for finding best CURIE matches. ## 0.4.10 - 2016-04-24 ### Changed - Add optimization for compacting keywords. ## 0.4.9 - 2016-04-23 ### Changed - Add optimizations for \_compactIri. ## 0.4.8 - 2016-04-14 ### Fixed - Revert es6-promise dependency to 2.x to avoid auto-polyfill behavior. ## 0.4.7 - 2016-04-14 ### Fixed - Testing document loader. ## 0.4.6 - 2016-03-02 ### Added - Add `headers` and `request` option for node doc loader. ### Changed - Include local tests. ## 0.4.5 - 2016-01-19 ### Fixed - N-Quads comments pattern. - Local tests. ## 0.4.4 - 2016-01-08 ### Fixed - Document cache in default node document loader is broken; disable until HTTP caching is implemented. ## 0.4.3 - 2016-01-05 ### Fixed - N-Quads may contain comments. ## 0.4.2 - 2015-10-12 ### Added - Add inputFormat and algorithm options to normalize. ### Changed - Add support for normalization test suite. - Support URDNA2015 normalization algorithm. - Add async scheduling of normalization algorithms. ### Fixed - Ignore null values in language maps. ## 0.4.1 - 2015-09-12 ### Changed - Ignore jsonld-request and pkginfo for browserify. ## 0.4.0 - 2015-09-12 ### Breaking Changes - "request" extension moved to [jsonld-request][]. This was done to simplify the core JSON-LD processing library. In particular, the extension pulled in RDFa processing code and related dependencies. The old method of using this extension of `jsonld.use('request')` is deprecated in favor of using the new module directly. - The `jsonld` tool moved to [jsonld-cli][]. This was also done to simplify the core JSON-LD processing library and because it uses the [jsonld-request][] module. ## 0.3.26 - 2015-09-01 ## Before 0.3.26 - See git history for changes. [jsonld-cli]: https://github.com/digitalbazaar/jsonld-cli [jsonld-request]: https://github.com/digitalbazaar/jsonld-request [rdf-canonize]: https://github.com/digitalbazaar/rdf-canonize [rdf-canonize-native]: https://github.com/digitalbazaar/rdf-canonize-native jsonld.js-4.0.1/LICENSE000066400000000000000000000044401401135163200144230ustar00rootroot00000000000000You may use the jsonld.js project under the terms of the BSD License. You are free to use this project in commercial projects as long as the copyright header is left intact. If you are a commercial entity and use this set of libraries in your commercial software then reasonable payment to Digital Bazaar, if you can afford it, is not required but is expected and would be appreciated. If this library saves you time, then it's saving you money. The cost of developing JSON-LD was on the order of several months of work and tens of thousands of dollars. We are attempting to strike a balance between helping the development community while not being taken advantage of by lucrative commercial entities for our efforts. ------------------------------------------------------------------------------- New BSD License (3-clause) Copyright (c) 2010, Digital Bazaar, Inc. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Digital Bazaar, Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. jsonld.js-4.0.1/README.md000066400000000000000000000346361401135163200147070ustar00rootroot00000000000000jsonld.js ========= [![Build status](https://img.shields.io/github/workflow/status/digitalbazaar/jsonld.js/Node.js%20CI)](https://github.com/digitalbazaar/jsonld.js/actions?query=workflow%3A%22Node.js+CI%22) [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/jsonld.js)](https://codecov.io/gh/digitalbazaar/jsonld.js) [![Dependency Status](https://img.shields.io/david/digitalbazaar/jsonld.js.svg)](https://david-dm.org/digitalbazaar/jsonld.js) Introduction ------------ This library is an implementation of the [JSON-LD][] specification in JavaScript. JSON, as specified in [RFC7159][], is a simple language for representing objects on the Web. Linked Data is a way of describing content across different documents or Web sites. Web resources are described using IRIs, and typically are dereferencable entities that may be used to find more information, creating a "Web of Knowledge". [JSON-LD][] is intended to be a simple publishing method for expressing not only Linked Data in JSON, but for adding semantics to existing JSON. JSON-LD is designed as a light-weight syntax that can be used to express Linked Data. It is primarily intended to be a way to express Linked Data in JavaScript and other Web-based programming environments. It is also useful when building interoperable Web Services and when storing Linked Data in JSON-based document storage engines. It is practical and designed to be as simple as possible, utilizing the large number of JSON parsers and existing code that is in use today. It is designed to be able to express key-value pairs, RDF data, [RDFa][] data, [Microformats][] data, and [Microdata][]. That is, it supports every major Web-based structured data model in use today. The syntax does not require many applications to change their JSON, but easily add meaning by adding context in a way that is either in-band or out-of-band. The syntax is designed to not disturb already deployed systems running on JSON, but provide a smooth migration path from JSON to JSON with added semantics. Finally, the format is intended to be fast to parse, fast to generate, stream-based and document-based processing compatible, and require a very small memory footprint in order to operate. Conformance ----------- This library aims to conform with the following: * [JSON-LD 1.0][], W3C Recommendation, 2014-01-16, and any [errata][] * [JSON-LD 1.0 Processing Algorithms and API][JSON-LD 1.0 API], W3C Recommendation, 2014-01-16, and any [errata][] * [JSON-LD 1.0 Framing][JSON-LD 1.0 Framing], Unofficial Draft, 2012-08-30 * [JSON-LD 1.1][JSON-LD CG 1.1], Draft Community Group Report, 2018-06-07 or [newer][JSON-LD CG latest] * [JSON-LD 1.1 Processing Algorithms and API][JSON-LD CG 1.1 API], Draft Community Group Report, 2018-06-07 or [newer][JSON-LD CG API latest] * [JSON-LD 1.1 Framing][JSON-LD CG 1.1 Framing], Draft Community Group Report, 2018-06-07 or [newer][JSON-LD CG Framing latest] * Community Group [test suite][] The [JSON-LD Working Group][JSON-LD WG] is now developing JSON-LD 1.1. Library updates to conform with newer specifications will happen as features stabilize and development time and resources permit. * [JSON-LD 1.1][JSON-LD WG 1.1], W3C Working Draft, 2018-12-14 or [newer][JSON-LD WG latest] * [JSON-LD 1.1 Processing Algorithms and API][JSON-LD WG 1.1 API], W3C Working Draft, 2018-12-14 or [newer][JSON-LD WG API latest] * [JSON-LD 1.1 Framing][JSON-LD WG 1.1 Framing], W3C Working Draft, 2018-12-14 or [newer][JSON-LD WG Framing latest] * Working Group [test suite][WG test suite] The [test runner][] is often updated to note or skip newer tests that are not yet supported. Installation ------------ ### Node.js + npm ``` npm install jsonld ``` ```js const jsonld = require('jsonld'); ``` ### Browser (AMD) + npm ``` npm install jsonld ``` Use your favorite technology to load `node_modules/dist/jsonld.min.js`. ### CDNJS CDN To use [CDNJS](https://cdnjs.com/) include this script tag: ```html ``` Check https://cdnjs.com/libraries/jsonld for the latest available version. ### jsDeliver CDN To use [jsDeliver](https://www.jsdelivr.com/) include this script tag: ```html ``` See https://www.jsdelivr.com/package/npm/jsonld for the latest available version. ### unpkg CDN To use [unpkg](https://unpkg.com/) include this script tag: ```html ``` See https://unpkg.com/jsonld/ for the latest available version. ### JSPM ``` jspm install npm:jsonld ``` ``` js import * as jsonld from 'jsonld'; // or import {promises} from 'jsonld'; // or import {JsonLdProcessor} from 'jsonld'; ``` ### Node.js native canonize bindings For specialized use cases there is an optional [rdf-canonize-native][] package available which provides a native implementation for `canonize()`. It is used by installing the package and setting the `useNative` option of `canonize()` to `true`. Before using this mode it is **highly recommended** to run benchmarks since the JavaScript implementation is often faster and the bindings add toolchain complexity. ``` npm install jsonld npm install rdf-canonize-native ``` Examples -------- Example data and context used throughout examples below: ```js const doc = { "http://schema.org/name": "Manu Sporny", "http://schema.org/url": {"@id": "http://manu.sporny.org/"}, "http://schema.org/image": {"@id": "http://manu.sporny.org/images/manu.png"} }; const context = { "name": "http://schema.org/name", "homepage": {"@id": "http://schema.org/url", "@type": "@id"}, "image": {"@id": "http://schema.org/image", "@type": "@id"} }; ``` ### [compact](http://json-ld.org/spec/latest/json-ld/#compacted-document-form) ```js // compact a document according to a particular context const compacted = await jsonld.compact(doc, context); console.log(JSON.stringify(compacted, null, 2)); /* Output: { "@context": {...}, "name": "Manu Sporny", "homepage": "http://manu.sporny.org/", "image": "http://manu.sporny.org/images/manu.png" } */ // compact using URLs const compacted = await jsonld.compact( 'http://example.org/doc', 'http://example.org/context', ...); ``` ### [expand](http://json-ld.org/spec/latest/json-ld/#expanded-document-form) ```js // expand a document, removing its context const expanded = await jsonld.expand(compacted); /* Output: { "http://schema.org/name": [{"@value": "Manu Sporny"}], "http://schema.org/url": [{"@id": "http://manu.sporny.org/"}], "http://schema.org/image": [{"@id": "http://manu.sporny.org/images/manu.png"}] } */ // expand using URLs const expanded = await jsonld.expand('http://example.org/doc', ...); ``` ### [flatten](http://json-ld.org/spec/latest/json-ld/#flattened-document-form) ```js // flatten a document const flattened = await jsonld.flatten(doc); // output has all deep-level trees flattened to the top-level ``` ### [frame](http://json-ld.org/spec/latest/json-ld-framing/#introduction) ```js // frame a document const framed = await jsonld.frame(doc, frame); // output transformed into a particular tree structure per the given frame ``` ### [canonize](http://json-ld.github.io/normalization/spec/) (normalize) ```js // canonize (normalize) a document using the RDF Dataset Normalization Algorithm // (URDNA2015), see: const canonized = await jsonld.canonize(doc, { algorithm: 'URDNA2015', format: 'application/n-quads' }); // canonized is a string that is a canonical representation of the document // that can be used for hashing, comparison, etc. ``` ### toRDF (N-Quads) ```js // serialize a document to N-Quads (RDF) const nquads = await jsonld.toRDF(doc, {format: 'application/n-quads'}); // nquads is a string of N-Quads ``` ### fromRDF (N-Quads) ```js // deserialize N-Quads (RDF) to JSON-LD const doc = await jsonld.fromRDF(nquads, {format: 'application/n-quads'}); // doc is JSON-LD ``` ### Custom RDF Parser ```js // register a custom synchronous RDF parser jsonld.registerRDFParser(contentType, input => { // parse input to a jsonld.js RDF dataset object... and return it return dataset; }); // register a custom promise-based RDF parser jsonld.registerRDFParser(contentType, async input => { // parse input into a jsonld.js RDF dataset object... return new Promise(...); }); ``` ### Custom Document Loader ```js // how to override the default document loader with a custom one -- for // example, one that uses pre-loaded contexts: // define a mapping of context URL => context doc const CONTEXTS = { "http://example.com": { "@context": ... }, ... }; // grab the built-in Node.js doc loader const nodeDocumentLoader = jsonld.documentLoaders.node(); // or grab the XHR one: jsonld.documentLoaders.xhr() // change the default document loader const customLoader = async (url, options) => { if(url in CONTEXTS) { return { contextUrl: null, // this is for a context via a link header document: CONTEXTS[url], // this is the actual document that was loaded documentUrl: url // this is the actual context URL after redirects }; } // call the default documentLoader return nodeDocumentLoader(url); }; jsonld.documentLoader = customLoader; // alternatively, pass the custom loader for just a specific call: const compacted = await jsonld.compact( doc, context, {documentLoader: customLoader}); ``` Related Modules --------------- * [jsonld-cli][]: A command line interface tool called `jsonld` that exposes most of the basic jsonld.js API. * [jsonld-request][]: A module that can read data from stdin, URLs, and files and in various formats and return JSON-LD. Commercial Support ------------------ Commercial support for this library is available upon request from [Digital Bazaar][]: support@digitalbazaar.com Source ------ The source code for the JavaScript implementation of the JSON-LD API is available at: http://github.com/digitalbazaar/jsonld.js Tests ----- This library includes a sample testing utility which may be used to verify that changes to the processor maintain the correct output. The main test suites are included in external repositories. Check out each of the following: https://github.com/w3c/json-ld-api https://github.com/w3c/json-ld-framing https://github.com/json-ld/json-ld.org https://github.com/json-ld/normalization They should be sibling directories of the jsonld.js directory or in a `test-suites` dir. To clone shallow copies into the `test-suites` dir you can use the following: npm run fetch-test-suites Node.js tests can be run with a simple command: npm test If you installed the test suites elsewhere, or wish to run other tests, use the `JSONLD_TESTS` environment var: JSONLD_TESTS="/tmp/org/test-suites /tmp/norm/tests" npm test This feature can be used to run the older json-ld.org test suite: JSONLD_TESTS=/tmp/json-ld.org/test-suite npm test Browser testing can be done with Karma: npm run test-karma npm run test-karma -- --browsers Firefox,Chrome Code coverage of node tests can be generated in `coverage/`: npm run coverage To display a full coverage report on the console from coverage data: npm run coverage-report The Mocha output reporter can be changed to min, dot, list, nyan, etc: REPORTER=dot npm test Remote context tests are also available: # run the context server in the background or another terminal node tests/remote-context-server.js JSONLD_TESTS=`pwd`/tests npm test To generate EARL reports: # generate the EARL report for Node.js EARL=earl-node.jsonld npm test # generate the EARL report for the browser EARL=earl-firefox.jsonld npm run test-karma -- --browser Firefox To generate an EARL report with the `json-ld-api` and `json-ld-framing` tests as used on the official [JSON-LD Processor Conformance][] page JSONLD_TESTS="`pwd`/../json-ld-api/tests `pwd`/../json-ld-framing/tests" EARL="jsonld-js-earl.jsonld" npm test The EARL `.jsonld` output can be converted to `.ttl` using the [rdf][] tool: rdf serialize jsonld-js-earl.jsonld --output-format turtle -o jsonld-js-earl.ttl Optionally follow the [report instructions](https://github.com/w3c/json-ld-api/tree/master/reports) to generate the HTML report for inspection. Maintainers can [submit](https://github.com/w3c/json-ld-api/pulls) updated results as needed. Benchmarks ---------- Benchmarks can be created from any manifest that the test system supports. Use a command line with a test suite and a benchmark flag: JSONLD_TESTS=/tmp/benchmark-manifest.jsonld JSONLD_BENCHMARK=1 npm test [Digital Bazaar]: https://digitalbazaar.com/ [JSON-LD 1.0 API]: http://www.w3.org/TR/2014/REC-json-ld-api-20140116/ [JSON-LD 1.0 Framing]: https://json-ld.org/spec/ED/json-ld-framing/20120830/ [JSON-LD 1.0]: http://www.w3.org/TR/2014/REC-json-ld-20140116/ [JSON-LD CG 1.1 API]: https://json-ld.org/spec/FCGS/json-ld-api/20180607/ [JSON-LD CG 1.1 Framing]: https://json-ld.org/spec/FCGS/json-ld-framing/20180607/ [JSON-LD CG 1.1]: https://json-ld.org/spec/FCGS/json-ld/20180607/ [JSON-LD CG API latest]: https://json-ld.org/spec/latest/json-ld-api/ [JSON-LD CG Framing latest]: https://json-ld.org/spec/latest/json-ld-framing/ [JSON-LD CG latest]: https://json-ld.org/spec/latest/json-ld/ [JSON-LD WG 1.1 API]: https://www.w3.org/TR/json-ld11-api/ [JSON-LD WG 1.1 Framing]: https://www.w3.org/TR/json-ld11-framing/ [JSON-LD WG 1.1]: https://www.w3.org/TR/json-ld11/ [JSON-LD WG API latest]: https://w3c.github.io/json-ld-api/ [JSON-LD WG Framing latest]: https://w3c.github.io/json-ld-framing/ [JSON-LD WG latest]: https://w3c.github.io/json-ld-syntax/ [JSON-LD Processor Conformance]: https://w3c.github.io/json-ld-api/reports [JSON-LD WG]: https://www.w3.org/2018/json-ld-wg/ [JSON-LD]: https://json-ld.org/ [Microdata]: http://www.w3.org/TR/microdata/ [Microformats]: http://microformats.org/ [RDFa]: http://www.w3.org/TR/rdfa-core/ [RFC7159]: http://tools.ietf.org/html/rfc7159 [WG test suite]: https://github.com/w3c/json-ld-api/tree/master/tests [errata]: http://www.w3.org/2014/json-ld-errata [jsonld-cli]: https://github.com/digitalbazaar/jsonld-cli [jsonld-request]: https://github.com/digitalbazaar/jsonld-request [rdf-canonize-native]: https://github.com/digitalbazaar/rdf-canonize-native [test runner]: https://github.com/digitalbazaar/jsonld.js/blob/master/tests/test-common.js [test suite]: https://github.com/json-ld/json-ld.org/tree/master/test-suite jsonld.js-4.0.1/karma.conf.js000066400000000000000000000113731401135163200157760ustar00rootroot00000000000000/** * Karam configuration for jsonld.js. * * Set dirs, manifests, or js to run: * JSONLD_TESTS="f1 f2 ..." * Output an EARL report: * EARL=filename * Bail with tests fail: * BAIL=true * * @author Dave Longley * @author David I. Lehn * * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. */ const webpack = require('webpack'); module.exports = function(config) { // bundler to test: webpack, browserify const bundler = process.env.BUNDLER || 'webpack'; const frameworks = ['mocha', 'server-side']; // main bundle preprocessors const preprocessors = ['babel']; if(bundler === 'browserify') { frameworks.push(bundler); preprocessors.push(bundler); } else if(bundler === 'webpack') { preprocessors.push(bundler); preprocessors.push('sourcemap'); } else { throw Error('Unknown bundler'); } config.set({ // 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, // list of files / patterns to load in the browser files: [ { pattern: 'tests/test-karma.js', watched: false, served: true, included: true } ], // list of files to exclude exclude: [ ], // preprocess matching files before serving them to the browser // available preprocessors: // https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { //'tests/*.js': ['webpack', 'babel'] //preprocessors 'tests/*.js': preprocessors }, webpack: { mode: 'production', devtool: 'inline-source-map', plugins: [ new webpack.DefinePlugin({ 'process.env.BAIL': JSON.stringify(process.env.BAIL), 'process.env.EARL': JSON.stringify(process.env.EARL), 'process.env.JSONLD_BENCHMARK': JSON.stringify(process.env.JSONLD_BENCHMARK), 'process.env.JSONLD_TESTS': JSON.stringify(process.env.JSONLD_TESTS), 'process.env.TEST_ROOT_DIR': JSON.stringify(__dirname), 'process.env.VERBOSE_SKIP': JSON.stringify(process.env.VERBOSE_SKIP) }) ], module: { rules: [ { test: /\.js$/, include: [{ // exclude node_modules by default exclude: /(node_modules)/ }, { // include specific packages include: [ /(node_modules\/canonicalize)/, /(node_modules\/lru-cache)/, /(node_modules\/rdf-canonize)/ ] }], use: { loader: 'babel-loader', options: { presets: ['@babel/preset-env'], plugins: [ [ '@babel/plugin-proposal-object-rest-spread', {useBuiltIns: true} ], '@babel/plugin-transform-modules-commonjs', '@babel/plugin-transform-runtime' ] } } } ] }, node: { Buffer: false, process: false, crypto: false, setImmediate: false } }, browserify: { debug: true //transform: ['uglifyify'] }, // test results reporter to use // possible values: 'dots', 'progress' // available reporters: https://npmjs.org/browse/keyword/karma-reporter //reporters: ['progress'], reporters: ['mocha'], // web server port 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: false, // start these browsers // available browser launchers: // https://npmjs.org/browse/keyword/karma-launcher //browsers: ['ChromeHeadless', 'Chrome', 'Firefox', 'Safari'], browsers: ['ChromeHeadless'], customLaunchers: { IE9: { base: 'IE', 'x-ua-compatible': 'IE=EmulateIE9' }, IE8: { base: 'IE', 'x-ua-compatible': 'IE=EmulateIE8' } }, // Continuous Integration mode // if true, Karma captures browsers, runs the tests and exits singleRun: true, // Concurrency level // how many browser should be started simultaneous concurrency: Infinity, // Mocha client: { mocha: { // increase from default 2s timeout: 10000, reporter: 'html', delay: true } }, // Proxied paths proxies: {} }); }; jsonld.js-4.0.1/lib/000077500000000000000000000000001401135163200141625ustar00rootroot00000000000000jsonld.js-4.0.1/lib/ContextResolver.js000066400000000000000000000165401401135163200176740ustar00rootroot00000000000000/* * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const { isArray: _isArray, isObject: _isObject, isString: _isString, } = require('./types'); const { asArray: _asArray } = require('./util'); const {prependBase} = require('./url'); const JsonLdError = require('./JsonLdError'); const ResolvedContext = require('./ResolvedContext'); const MAX_CONTEXT_URLS = 10; module.exports = class ContextResolver { /** * Creates a ContextResolver. * * @param sharedCache a shared LRU cache with `get` and `set` APIs. */ constructor({sharedCache}) { this.perOpCache = new Map(); this.sharedCache = sharedCache; } async resolve({ activeCtx, context, documentLoader, base, cycles = new Set() }) { // process `@context` if(context && _isObject(context) && context['@context']) { context = context['@context']; } // context is one or more contexts context = _asArray(context); // resolve each context in the array const allResolved = []; for(const ctx of context) { if(_isString(ctx)) { // see if `ctx` has been resolved before... let resolved = this._get(ctx); if(!resolved) { // not resolved yet, resolve resolved = await this._resolveRemoteContext( {activeCtx, url: ctx, documentLoader, base, cycles}); } // add to output and continue if(_isArray(resolved)) { allResolved.push(...resolved); } else { allResolved.push(resolved); } continue; } if(ctx === null) { // handle `null` context, nothing to cache allResolved.push(new ResolvedContext({document: null})); continue; } if(!_isObject(ctx)) { _throwInvalidLocalContext(context); } // context is an object, get/create `ResolvedContext` for it const key = JSON.stringify(ctx); let resolved = this._get(key); if(!resolved) { // create a new static `ResolvedContext` and cache it resolved = new ResolvedContext({document: ctx}); this._cacheResolvedContext({key, resolved, tag: 'static'}); } allResolved.push(resolved); } return allResolved; } _get(key) { // get key from per operation cache; no `tag` is used with this cache so // any retrieved context will always be the same during a single operation let resolved = this.perOpCache.get(key); if(!resolved) { // see if the shared cache has a `static` entry for this URL const tagMap = this.sharedCache.get(key); if(tagMap) { resolved = tagMap.get('static'); if(resolved) { this.perOpCache.set(key, resolved); } } } return resolved; } _cacheResolvedContext({key, resolved, tag}) { this.perOpCache.set(key, resolved); if(tag !== undefined) { let tagMap = this.sharedCache.get(key); if(!tagMap) { tagMap = new Map(); this.sharedCache.set(key, tagMap); } tagMap.set(tag, resolved); } return resolved; } async _resolveRemoteContext({activeCtx, url, documentLoader, base, cycles}) { // resolve relative URL and fetch context url = prependBase(base, url); const {context, remoteDoc} = await this._fetchContext( {activeCtx, url, documentLoader, cycles}); // update base according to remote document and resolve any relative URLs base = remoteDoc.documentUrl || url; _resolveContextUrls({context, base}); // resolve, cache, and return context const resolved = await this.resolve( {activeCtx, context, documentLoader, base, cycles}); this._cacheResolvedContext({key: url, resolved, tag: remoteDoc.tag}); return resolved; } async _fetchContext({activeCtx, url, documentLoader, cycles}) { // check for max context URLs fetched during a resolve operation if(cycles.size > MAX_CONTEXT_URLS) { throw new JsonLdError( 'Maximum number of @context URLs exceeded.', 'jsonld.ContextUrlError', { code: activeCtx.processingMode === 'json-ld-1.0' ? 'loading remote context failed' : 'context overflow', max: MAX_CONTEXT_URLS }); } // check for context URL cycle // shortcut to avoid extra work that would eventually hit the max above if(cycles.has(url)) { throw new JsonLdError( 'Cyclical @context URLs detected.', 'jsonld.ContextUrlError', { code: activeCtx.processingMode === 'json-ld-1.0' ? 'recursive context inclusion' : 'context overflow', url }); } // track cycles cycles.add(url); let context; let remoteDoc; try { remoteDoc = await documentLoader(url); context = remoteDoc.document || null; // parse string context as JSON if(_isString(context)) { context = JSON.parse(context); } } catch(e) { throw new JsonLdError( 'Dereferencing a URL did not result in a valid JSON-LD object. ' + 'Possible causes are an inaccessible URL perhaps due to ' + 'a same-origin policy (ensure the server uses CORS if you are ' + 'using client-side JavaScript), too many redirects, a ' + 'non-JSON response, or more than one HTTP Link Header was ' + 'provided for a remote context.', 'jsonld.InvalidUrl', {code: 'loading remote context failed', url, cause: e}); } // ensure ctx is an object if(!_isObject(context)) { throw new JsonLdError( 'Dereferencing a URL did not result in a JSON object. The ' + 'response was valid JSON, but it was not a JSON object.', 'jsonld.InvalidUrl', {code: 'invalid remote context', url}); } // use empty context if no @context key is present if(!('@context' in context)) { context = {'@context': {}}; } else { context = {'@context': context['@context']}; } // append @context URL to context if given if(remoteDoc.contextUrl) { if(!_isArray(context['@context'])) { context['@context'] = [context['@context']]; } context['@context'].push(remoteDoc.contextUrl); } return {context, remoteDoc}; } }; function _throwInvalidLocalContext(ctx) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context must be an object.', 'jsonld.SyntaxError', { code: 'invalid local context', context: ctx }); } /** * Resolve all relative `@context` URLs in the given context by inline * replacing them with absolute URLs. * * @param context the context. * @param base the base IRI to use to resolve relative IRIs. */ function _resolveContextUrls({context, base}) { if(!context) { return; } const ctx = context['@context']; if(_isString(ctx)) { context['@context'] = prependBase(base, ctx); return; } if(_isArray(ctx)) { for(let i = 0; i < ctx.length; ++i) { const element = ctx[i]; if(_isString(element)) { ctx[i] = prependBase(base, element); continue; } if(_isObject(element)) { _resolveContextUrls({context: {'@context': element}, base}); } } return; } if(!_isObject(ctx)) { // no @context URLs can be found in non-object return; } // ctx is an object, resolve any context URLs in terms for(const term in ctx) { _resolveContextUrls({context: ctx[term], base}); } } jsonld.js-4.0.1/lib/JsonLdError.js000066400000000000000000000010021401135163200167140ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; module.exports = class JsonLdError extends Error { /** * Creates a JSON-LD Error. * * @param msg the error message. * @param type the error type. * @param details the error details. */ constructor( message = 'An unspecified JSON-LD error occurred.', name = 'jsonld.Error', details = {}) { super(message); this.name = name; this.message = message; this.details = details; } }; jsonld.js-4.0.1/lib/JsonLdProcessor.js000066400000000000000000000027151401135163200176160ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; module.exports = jsonld => { class JsonLdProcessor { toString() { return '[object JsonLdProcessor]'; } } Object.defineProperty(JsonLdProcessor, 'prototype', { writable: false, enumerable: false }); Object.defineProperty(JsonLdProcessor.prototype, 'constructor', { writable: true, enumerable: false, configurable: true, value: JsonLdProcessor }); // The Web IDL test harness will check the number of parameters defined in // the functions below. The number of parameters must exactly match the // required (non-optional) parameters of the JsonLdProcessor interface as // defined here: // https://www.w3.org/TR/json-ld-api/#the-jsonldprocessor-interface JsonLdProcessor.compact = function(input, ctx) { if(arguments.length < 2) { return Promise.reject( new TypeError('Could not compact, too few arguments.')); } return jsonld.compact(input, ctx); }; JsonLdProcessor.expand = function(input) { if(arguments.length < 1) { return Promise.reject( new TypeError('Could not expand, too few arguments.')); } return jsonld.expand(input); }; JsonLdProcessor.flatten = function(input) { if(arguments.length < 1) { return Promise.reject( new TypeError('Could not flatten, too few arguments.')); } return jsonld.flatten(input); }; return JsonLdProcessor; }; jsonld.js-4.0.1/lib/NQuads.js000066400000000000000000000002611401135163200157120ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; // TODO: move `NQuads` to its own package module.exports = require('rdf-canonize').NQuads; jsonld.js-4.0.1/lib/RequestQueue.js000066400000000000000000000013741401135163200171620ustar00rootroot00000000000000/* * Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; module.exports = class RequestQueue { /** * Creates a simple queue for requesting documents. */ constructor() { this._requests = {}; } wrapLoader(loader) { const self = this; self._loader = loader; return function(/* url */) { return self.add.apply(self, arguments); }; } async add(url) { let promise = this._requests[url]; if(promise) { // URL already queued, wait for it to load return Promise.resolve(promise); } // queue URL and load it promise = this._requests[url] = this._loader(url); try { return await promise; } finally { delete this._requests[url]; } } }; jsonld.js-4.0.1/lib/ResolvedContext.js000066400000000000000000000013001401135163200176420ustar00rootroot00000000000000/* * Copyright (c) 2019 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const LRU = require('lru-cache'); const MAX_ACTIVE_CONTEXTS = 10; module.exports = class ResolvedContext { /** * Creates a ResolvedContext. * * @param document the context document. */ constructor({document}) { this.document = document; // TODO: enable customization of processed context cache // TODO: limit based on size of processed contexts vs. number of them this.cache = new LRU({max: MAX_ACTIVE_CONTEXTS}); } getProcessed(activeCtx) { return this.cache.get(activeCtx); } setProcessed(activeCtx, processedCtx) { this.cache.set(activeCtx, processedCtx); } }; jsonld.js-4.0.1/lib/compact.js000066400000000000000000001136211401135163200161520ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const JsonLdError = require('./JsonLdError'); const { isArray: _isArray, isObject: _isObject, isString: _isString, isUndefined: _isUndefined } = require('./types'); const { isList: _isList, isValue: _isValue, isGraph: _isGraph, isSimpleGraph: _isSimpleGraph, isSubjectReference: _isSubjectReference } = require('./graphTypes'); const { expandIri: _expandIri, getContextValue: _getContextValue, isKeyword: _isKeyword, process: _processContext, processingMode: _processingMode } = require('./context'); const { removeBase: _removeBase, prependBase: _prependBase } = require('./url'); const { addValue: _addValue, asArray: _asArray, compareShortestLeast: _compareShortestLeast } = require('./util'); const api = {}; module.exports = api; /** * Recursively compacts an element using the given active context. All values * must be in expanded form before this method is called. * * @param activeCtx the active context to use. * @param activeProperty the compacted property associated with the element * to compact, null for none. * @param element the element to compact. * @param options the compaction options. * @param compactionMap the compaction map to use. * * @return a promise that resolves to the compacted value. */ api.compact = async ({ activeCtx, activeProperty = null, element, options = {}, compactionMap = () => undefined }) => { // recursively compact array if(_isArray(element)) { let rval = []; for(let i = 0; i < element.length; ++i) { // compact, dropping any null values unless custom mapped let compacted = await api.compact({ activeCtx, activeProperty, element: element[i], options, compactionMap }); if(compacted === null) { compacted = await compactionMap({ unmappedValue: element[i], activeCtx, activeProperty, parent: element, index: i, options }); if(compacted === undefined) { continue; } } rval.push(compacted); } if(options.compactArrays && rval.length === 1) { // use single element if no container is specified const container = _getContextValue( activeCtx, activeProperty, '@container') || []; if(container.length === 0) { rval = rval[0]; } } return rval; } // use any scoped context on activeProperty const ctx = _getContextValue(activeCtx, activeProperty, '@context'); if(!_isUndefined(ctx)) { activeCtx = await _processContext({ activeCtx, localCtx: ctx, propagate: true, overrideProtected: true, options }); } // recursively compact object if(_isObject(element)) { if(options.link && '@id' in element && options.link.hasOwnProperty(element['@id'])) { // check for a linked element to reuse const linked = options.link[element['@id']]; for(let i = 0; i < linked.length; ++i) { if(linked[i].expanded === element) { return linked[i].compacted; } } } // do value compaction on @values and subject references if(_isValue(element) || _isSubjectReference(element)) { const rval = api.compactValue({activeCtx, activeProperty, value: element, options}); if(options.link && _isSubjectReference(element)) { // store linked element if(!(options.link.hasOwnProperty(element['@id']))) { options.link[element['@id']] = []; } options.link[element['@id']].push({expanded: element, compacted: rval}); } return rval; } // if expanded property is @list and we're contained within a list // container, recursively compact this item to an array if(_isList(element)) { const container = _getContextValue( activeCtx, activeProperty, '@container') || []; if(container.includes('@list')) { return api.compact({ activeCtx, activeProperty, element: element['@list'], options, compactionMap }); } } // FIXME: avoid misuse of active property as an expanded property? const insideReverse = (activeProperty === '@reverse'); const rval = {}; // original context before applying property-scoped and local contexts const inputCtx = activeCtx; // revert to previous context, if there is one, // and element is not a value object or a node reference if(!_isValue(element) && !_isSubjectReference(element)) { activeCtx = activeCtx.revertToPreviousContext(); } // apply property-scoped context after reverting term-scoped context const propertyScopedCtx = _getContextValue(inputCtx, activeProperty, '@context'); if(!_isUndefined(propertyScopedCtx)) { activeCtx = await _processContext({ activeCtx, localCtx: propertyScopedCtx, propagate: true, overrideProtected: true, options }); } if(options.link && '@id' in element) { // store linked element if(!options.link.hasOwnProperty(element['@id'])) { options.link[element['@id']] = []; } options.link[element['@id']].push({expanded: element, compacted: rval}); } // apply any context defined on an alias of @type // if key is @type and any compacted value is a term having a local // context, overlay that context let types = element['@type'] || []; if(types.length > 1) { types = Array.from(types).sort(); } // find all type-scoped contexts based on current context, prior to // updating it const typeContext = activeCtx; for(const type of types) { const compactedType = api.compactIri( {activeCtx: typeContext, iri: type, relativeTo: {vocab: true}}); // Use any type-scoped context defined on this value const ctx = _getContextValue(inputCtx, compactedType, '@context'); if(!_isUndefined(ctx)) { activeCtx = await _processContext({ activeCtx, localCtx: ctx, options, propagate: false }); } } // process element keys in order const keys = Object.keys(element).sort(); for(const expandedProperty of keys) { const expandedValue = element[expandedProperty]; // compact @id if(expandedProperty === '@id') { let compactedValue = _asArray(expandedValue).map( expandedIri => api.compactIri({ activeCtx, iri: expandedIri, relativeTo: {vocab: false}, base: options.base })); if(compactedValue.length === 1) { compactedValue = compactedValue[0]; } // use keyword alias and add value const alias = api.compactIri( {activeCtx, iri: '@id', relativeTo: {vocab: true}}); rval[alias] = compactedValue; continue; } // compact @type(s) if(expandedProperty === '@type') { // resolve type values against previous context let compactedValue = _asArray(expandedValue).map( expandedIri => api.compactIri({ activeCtx: inputCtx, iri: expandedIri, relativeTo: {vocab: true} })); if(compactedValue.length === 1) { compactedValue = compactedValue[0]; } // use keyword alias and add value const alias = api.compactIri( {activeCtx, iri: '@type', relativeTo: {vocab: true}}); const container = _getContextValue( activeCtx, alias, '@container') || []; // treat as array for @type if @container includes @set const typeAsSet = container.includes('@set') && _processingMode(activeCtx, 1.1); const isArray = typeAsSet || (_isArray(compactedValue) && expandedValue.length === 0); _addValue(rval, alias, compactedValue, {propertyIsArray: isArray}); continue; } // handle @reverse if(expandedProperty === '@reverse') { // recursively compact expanded value const compactedValue = await api.compact({ activeCtx, activeProperty: '@reverse', element: expandedValue, options, compactionMap }); // handle double-reversed properties for(const compactedProperty in compactedValue) { if(activeCtx.mappings.has(compactedProperty) && activeCtx.mappings.get(compactedProperty).reverse) { const value = compactedValue[compactedProperty]; const container = _getContextValue( activeCtx, compactedProperty, '@container') || []; const useArray = ( container.includes('@set') || !options.compactArrays); _addValue( rval, compactedProperty, value, {propertyIsArray: useArray}); delete compactedValue[compactedProperty]; } } if(Object.keys(compactedValue).length > 0) { // use keyword alias and add value const alias = api.compactIri({ activeCtx, iri: expandedProperty, relativeTo: {vocab: true} }); _addValue(rval, alias, compactedValue); } continue; } if(expandedProperty === '@preserve') { // compact using activeProperty const compactedValue = await api.compact({ activeCtx, activeProperty, element: expandedValue, options, compactionMap }); if(!(_isArray(compactedValue) && compactedValue.length === 0)) { _addValue(rval, expandedProperty, compactedValue); } continue; } // handle @index property if(expandedProperty === '@index') { // drop @index if inside an @index container const container = _getContextValue( activeCtx, activeProperty, '@container') || []; if(container.includes('@index')) { continue; } // use keyword alias and add value const alias = api.compactIri({ activeCtx, iri: expandedProperty, relativeTo: {vocab: true} }); _addValue(rval, alias, expandedValue); continue; } // skip array processing for keywords that aren't // @graph, @list, or @included if(expandedProperty !== '@graph' && expandedProperty !== '@list' && expandedProperty !== '@included' && _isKeyword(expandedProperty)) { // use keyword alias and add value as is const alias = api.compactIri({ activeCtx, iri: expandedProperty, relativeTo: {vocab: true} }); _addValue(rval, alias, expandedValue); continue; } // Note: expanded value must be an array due to expansion algorithm. if(!_isArray(expandedValue)) { throw new JsonLdError( 'JSON-LD expansion error; expanded value must be an array.', 'jsonld.SyntaxError'); } // preserve empty arrays if(expandedValue.length === 0) { const itemActiveProperty = api.compactIri({ activeCtx, iri: expandedProperty, value: expandedValue, relativeTo: {vocab: true}, reverse: insideReverse }); const nestProperty = activeCtx.mappings.has(itemActiveProperty) ? activeCtx.mappings.get(itemActiveProperty)['@nest'] : null; let nestResult = rval; if(nestProperty) { _checkNestProperty(activeCtx, nestProperty, options); if(!_isObject(rval[nestProperty])) { rval[nestProperty] = {}; } nestResult = rval[nestProperty]; } _addValue( nestResult, itemActiveProperty, expandedValue, { propertyIsArray: true }); } // recusively process array values for(const expandedItem of expandedValue) { // compact property and get container type const itemActiveProperty = api.compactIri({ activeCtx, iri: expandedProperty, value: expandedItem, relativeTo: {vocab: true}, reverse: insideReverse }); // if itemActiveProperty is a @nest property, add values to nestResult, // otherwise rval const nestProperty = activeCtx.mappings.has(itemActiveProperty) ? activeCtx.mappings.get(itemActiveProperty)['@nest'] : null; let nestResult = rval; if(nestProperty) { _checkNestProperty(activeCtx, nestProperty, options); if(!_isObject(rval[nestProperty])) { rval[nestProperty] = {}; } nestResult = rval[nestProperty]; } const container = _getContextValue( activeCtx, itemActiveProperty, '@container') || []; // get simple @graph or @list value if appropriate const isGraph = _isGraph(expandedItem); const isList = _isList(expandedItem); let inner; if(isList) { inner = expandedItem['@list']; } else if(isGraph) { inner = expandedItem['@graph']; } // recursively compact expanded item let compactedItem = await api.compact({ activeCtx, activeProperty: itemActiveProperty, element: (isList || isGraph) ? inner : expandedItem, options, compactionMap }); // handle @list if(isList) { // ensure @list value is an array if(!_isArray(compactedItem)) { compactedItem = [compactedItem]; } if(!container.includes('@list')) { // wrap using @list alias compactedItem = { [api.compactIri({ activeCtx, iri: '@list', relativeTo: {vocab: true} })]: compactedItem }; // include @index from expanded @list, if any if('@index' in expandedItem) { compactedItem[api.compactIri({ activeCtx, iri: '@index', relativeTo: {vocab: true} })] = expandedItem['@index']; } } else { _addValue(nestResult, itemActiveProperty, compactedItem, { valueIsArray: true, allowDuplicate: true }); continue; } } // Graph object compaction cases if(isGraph) { if(container.includes('@graph') && (container.includes('@id') || container.includes('@index') && _isSimpleGraph(expandedItem))) { // get or create the map object let mapObject; if(nestResult.hasOwnProperty(itemActiveProperty)) { mapObject = nestResult[itemActiveProperty]; } else { nestResult[itemActiveProperty] = mapObject = {}; } // index on @id or @index or alias of @none const key = (container.includes('@id') ? expandedItem['@id'] : expandedItem['@index']) || api.compactIri({activeCtx, iri: '@none', relativeTo: {vocab: true}}); // add compactedItem to map, using value of `@id` or a new blank // node identifier _addValue( mapObject, key, compactedItem, { propertyIsArray: (!options.compactArrays || container.includes('@set')) }); } else if(container.includes('@graph') && _isSimpleGraph(expandedItem)) { // container includes @graph but not @id or @index and value is a // simple graph object add compact value // if compactedItem contains multiple values, it is wrapped in // `@included` if(_isArray(compactedItem) && compactedItem.length > 1) { compactedItem = {'@included': compactedItem}; } _addValue( nestResult, itemActiveProperty, compactedItem, { propertyIsArray: (!options.compactArrays || container.includes('@set')) }); } else { // wrap using @graph alias, remove array if only one item and // compactArrays not set if(_isArray(compactedItem) && compactedItem.length === 1 && options.compactArrays) { compactedItem = compactedItem[0]; } compactedItem = { [api.compactIri({ activeCtx, iri: '@graph', relativeTo: {vocab: true} })]: compactedItem }; // include @id from expanded graph, if any if('@id' in expandedItem) { compactedItem[api.compactIri({ activeCtx, iri: '@id', relativeTo: {vocab: true} })] = expandedItem['@id']; } // include @index from expanded graph, if any if('@index' in expandedItem) { compactedItem[api.compactIri({ activeCtx, iri: '@index', relativeTo: {vocab: true} })] = expandedItem['@index']; } _addValue( nestResult, itemActiveProperty, compactedItem, { propertyIsArray: (!options.compactArrays || container.includes('@set')) }); } } else if(container.includes('@language') || container.includes('@index') || container.includes('@id') || container.includes('@type')) { // handle language and index maps // get or create the map object let mapObject; if(nestResult.hasOwnProperty(itemActiveProperty)) { mapObject = nestResult[itemActiveProperty]; } else { nestResult[itemActiveProperty] = mapObject = {}; } let key; if(container.includes('@language')) { // if container is a language map, simplify compacted value to // a simple string if(_isValue(compactedItem)) { compactedItem = compactedItem['@value']; } key = expandedItem['@language']; } else if(container.includes('@index')) { const indexKey = _getContextValue( activeCtx, itemActiveProperty, '@index') || '@index'; const containerKey = api.compactIri( {activeCtx, iri: indexKey, relativeTo: {vocab: true}}); if(indexKey === '@index') { key = expandedItem['@index']; delete compactedItem[containerKey]; } else { let others; [key, ...others] = _asArray(compactedItem[indexKey] || []); if(!_isString(key)) { // Will use @none if it isn't a string. key = null; } else { switch(others.length) { case 0: delete compactedItem[indexKey]; break; case 1: compactedItem[indexKey] = others[0]; break; default: compactedItem[indexKey] = others; break; } } } } else if(container.includes('@id')) { const idKey = api.compactIri({activeCtx, iri: '@id', relativeTo: {vocab: true}}); key = compactedItem[idKey]; delete compactedItem[idKey]; } else if(container.includes('@type')) { const typeKey = api.compactIri({ activeCtx, iri: '@type', relativeTo: {vocab: true} }); let types; [key, ...types] = _asArray(compactedItem[typeKey] || []); switch(types.length) { case 0: delete compactedItem[typeKey]; break; case 1: compactedItem[typeKey] = types[0]; break; default: compactedItem[typeKey] = types; break; } // If compactedItem contains a single entry // whose key maps to @id, recompact without @type if(Object.keys(compactedItem).length === 1 && '@id' in expandedItem) { compactedItem = await api.compact({ activeCtx, activeProperty: itemActiveProperty, element: {'@id': expandedItem['@id']}, options, compactionMap }); } } // if compacting this value which has no key, index on @none if(!key) { key = api.compactIri({activeCtx, iri: '@none', relativeTo: {vocab: true}}); } // add compact value to map object using key from expanded value // based on the container type _addValue( mapObject, key, compactedItem, { propertyIsArray: container.includes('@set') }); } else { // use an array if: compactArrays flag is false, // @container is @set or @list , value is an empty // array, or key is @graph const isArray = (!options.compactArrays || container.includes('@set') || container.includes('@list') || (_isArray(compactedItem) && compactedItem.length === 0) || expandedProperty === '@list' || expandedProperty === '@graph'); // add compact value _addValue( nestResult, itemActiveProperty, compactedItem, {propertyIsArray: isArray}); } } } return rval; } // only primitives remain which are already compact return element; }; /** * Compacts an IRI or keyword into a term or prefix if it can be. If the * IRI has an associated value it may be passed. * * @param activeCtx the active context to use. * @param iri the IRI to compact. * @param value the value to check or null. * @param relativeTo options for how to compact IRIs: * vocab: true to split after @vocab, false not to. * @param reverse true if a reverse property is being compacted, false if not. * @param base the absolute URL to use for compacting document-relative IRIs. * * @return the compacted term, prefix, keyword alias, or the original IRI. */ api.compactIri = ({ activeCtx, iri, value = null, relativeTo = {vocab: false}, reverse = false, base = null }) => { // can't compact null if(iri === null) { return iri; } // if context is from a property term scoped context composed with a // type-scoped context, then use the previous context instead if(activeCtx.isPropertyTermScoped && activeCtx.previousContext) { activeCtx = activeCtx.previousContext; } const inverseCtx = activeCtx.getInverse(); // if term is a keyword, it may be compacted to a simple alias if(_isKeyword(iri) && iri in inverseCtx && '@none' in inverseCtx[iri] && '@type' in inverseCtx[iri]['@none'] && '@none' in inverseCtx[iri]['@none']['@type']) { return inverseCtx[iri]['@none']['@type']['@none']; } // use inverse context to pick a term if iri is relative to vocab if(relativeTo.vocab && iri in inverseCtx) { const defaultLanguage = activeCtx['@language'] || '@none'; // prefer @index if available in value const containers = []; if(_isObject(value) && '@index' in value && !('@graph' in value)) { containers.push('@index', '@index@set'); } // if value is a preserve object, use its value if(_isObject(value) && '@preserve' in value) { value = value['@preserve'][0]; } // prefer most specific container including @graph, prefering @set // variations if(_isGraph(value)) { // favor indexmap if the graph is indexed if('@index' in value) { containers.push( '@graph@index', '@graph@index@set', '@index', '@index@set'); } // favor idmap if the graph is has an @id if('@id' in value) { containers.push( '@graph@id', '@graph@id@set'); } containers.push('@graph', '@graph@set', '@set'); // allow indexmap if the graph is not indexed if(!('@index' in value)) { containers.push( '@graph@index', '@graph@index@set', '@index', '@index@set'); } // allow idmap if the graph does not have an @id if(!('@id' in value)) { containers.push('@graph@id', '@graph@id@set'); } } else if(_isObject(value) && !_isValue(value)) { containers.push('@id', '@id@set', '@type', '@set@type'); } // defaults for term selection based on type/language let typeOrLanguage = '@language'; let typeOrLanguageValue = '@null'; if(reverse) { typeOrLanguage = '@type'; typeOrLanguageValue = '@reverse'; containers.push('@set'); } else if(_isList(value)) { // choose the most specific term that works for all elements in @list // only select @list containers if @index is NOT in value if(!('@index' in value)) { containers.push('@list'); } const list = value['@list']; if(list.length === 0) { // any empty list can be matched against any term that uses the // @list container regardless of @type or @language typeOrLanguage = '@any'; typeOrLanguageValue = '@none'; } else { let commonLanguage = (list.length === 0) ? defaultLanguage : null; let commonType = null; for(let i = 0; i < list.length; ++i) { const item = list[i]; let itemLanguage = '@none'; let itemType = '@none'; if(_isValue(item)) { if('@direction' in item) { const lang = (item['@language'] || '').toLowerCase(); const dir = item['@direction']; itemLanguage = `${lang}_${dir}`; } else if('@language' in item) { itemLanguage = item['@language'].toLowerCase(); } else if('@type' in item) { itemType = item['@type']; } else { // plain literal itemLanguage = '@null'; } } else { itemType = '@id'; } if(commonLanguage === null) { commonLanguage = itemLanguage; } else if(itemLanguage !== commonLanguage && _isValue(item)) { commonLanguage = '@none'; } if(commonType === null) { commonType = itemType; } else if(itemType !== commonType) { commonType = '@none'; } // there are different languages and types in the list, so choose // the most generic term, no need to keep iterating the list if(commonLanguage === '@none' && commonType === '@none') { break; } } commonLanguage = commonLanguage || '@none'; commonType = commonType || '@none'; if(commonType !== '@none') { typeOrLanguage = '@type'; typeOrLanguageValue = commonType; } else { typeOrLanguageValue = commonLanguage; } } } else { if(_isValue(value)) { if('@language' in value && !('@index' in value)) { containers.push('@language', '@language@set'); typeOrLanguageValue = value['@language']; const dir = value['@direction']; if(dir) { typeOrLanguageValue = `${typeOrLanguageValue}_${dir}`; } } else if('@direction' in value && !('@index' in value)) { typeOrLanguageValue = `_${value['@direction']}`; } else if('@type' in value) { typeOrLanguage = '@type'; typeOrLanguageValue = value['@type']; } } else { typeOrLanguage = '@type'; typeOrLanguageValue = '@id'; } containers.push('@set'); } // do term selection containers.push('@none'); // an index map can be used to index values using @none, so add as a low // priority if(_isObject(value) && !('@index' in value)) { // allow indexing even if no @index present containers.push('@index', '@index@set'); } // values without type or language can use @language map if(_isValue(value) && Object.keys(value).length === 1) { // allow indexing even if no @index present containers.push('@language', '@language@set'); } const term = _selectTerm( activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue); if(term !== null) { return term; } } // no term match, use @vocab if available if(relativeTo.vocab) { if('@vocab' in activeCtx) { // determine if vocab is a prefix of the iri const vocab = activeCtx['@vocab']; if(iri.indexOf(vocab) === 0 && iri !== vocab) { // use suffix as relative iri if it is not a term in the active context const suffix = iri.substr(vocab.length); if(!activeCtx.mappings.has(suffix)) { return suffix; } } } } // no term or @vocab match, check for possible CURIEs let choice = null; // TODO: make FastCurieMap a class with a method to do this lookup const partialMatches = []; let iriMap = activeCtx.fastCurieMap; // check for partial matches of against `iri`, which means look until // iri.length - 1, not full length const maxPartialLength = iri.length - 1; for(let i = 0; i < maxPartialLength && iri[i] in iriMap; ++i) { iriMap = iriMap[iri[i]]; if('' in iriMap) { partialMatches.push(iriMap[''][0]); } } // check partial matches in reverse order to prefer longest ones first for(let i = partialMatches.length - 1; i >= 0; --i) { const entry = partialMatches[i]; const terms = entry.terms; for(const term of terms) { // a CURIE is usable if: // 1. it has no mapping, OR // 2. value is null, which means we're not compacting an @value, AND // the mapping matches the IRI const curie = term + ':' + iri.substr(entry.iri.length); const isUsableCurie = (activeCtx.mappings.get(term)._prefix && (!activeCtx.mappings.has(curie) || (value === null && activeCtx.mappings.get(curie)['@id'] === iri))); // select curie if it is shorter or the same length but lexicographically // less than the current choice if(isUsableCurie && (choice === null || _compareShortestLeast(curie, choice) < 0)) { choice = curie; } } } // return chosen curie if(choice !== null) { return choice; } // If iri could be confused with a compact IRI using a term in this context, // signal an error for(const [term, td] of activeCtx.mappings) { if(td && td._prefix && iri.startsWith(term + ':')) { throw new JsonLdError( `Absolute IRI "${iri}" confused with prefix "${term}".`, 'jsonld.SyntaxError', {code: 'IRI confused with prefix', context: activeCtx}); } } // compact IRI relative to base if(!relativeTo.vocab) { if('@base' in activeCtx) { if(!activeCtx['@base']) { // The None case preserves rval as potentially relative return iri; } else { return _removeBase(_prependBase(base, activeCtx['@base']), iri); } } else { return _removeBase(base, iri); } } // return IRI as is return iri; }; /** * Performs value compaction on an object with '@value' or '@id' as the only * property. * * @param activeCtx the active context. * @param activeProperty the active property that points to the value. * @param value the value to compact. * @param {Object} [options] - processing options. * * @return the compaction result. */ api.compactValue = ({activeCtx, activeProperty, value, options}) => { // value is a @value if(_isValue(value)) { // get context rules const type = _getContextValue(activeCtx, activeProperty, '@type'); const language = _getContextValue(activeCtx, activeProperty, '@language'); const direction = _getContextValue(activeCtx, activeProperty, '@direction'); const container = _getContextValue(activeCtx, activeProperty, '@container') || []; // whether or not the value has an @index that must be preserved const preserveIndex = '@index' in value && !container.includes('@index'); // if there's no @index to preserve ... if(!preserveIndex && type !== '@none') { // matching @type or @language specified in context, compact value if(value['@type'] === type) { return value['@value']; } if('@language' in value && value['@language'] === language && '@direction' in value && value['@direction'] === direction) { return value['@value']; } if('@language' in value && value['@language'] === language) { return value['@value']; } if('@direction' in value && value['@direction'] === direction) { return value['@value']; } } // return just the value of @value if all are true: // 1. @value is the only key or @index isn't being preserved // 2. there is no default language or @value is not a string or // the key has a mapping with a null @language const keyCount = Object.keys(value).length; const isValueOnlyKey = (keyCount === 1 || (keyCount === 2 && '@index' in value && !preserveIndex)); const hasDefaultLanguage = ('@language' in activeCtx); const isValueString = _isString(value['@value']); const hasNullMapping = (activeCtx.mappings.has(activeProperty) && activeCtx.mappings.get(activeProperty)['@language'] === null); if(isValueOnlyKey && type !== '@none' && (!hasDefaultLanguage || !isValueString || hasNullMapping)) { return value['@value']; } const rval = {}; // preserve @index if(preserveIndex) { rval[api.compactIri({ activeCtx, iri: '@index', relativeTo: {vocab: true} })] = value['@index']; } if('@type' in value) { // compact @type IRI rval[api.compactIri({ activeCtx, iri: '@type', relativeTo: {vocab: true} })] = api.compactIri( {activeCtx, iri: value['@type'], relativeTo: {vocab: true}}); } else if('@language' in value) { // alias @language rval[api.compactIri({ activeCtx, iri: '@language', relativeTo: {vocab: true} })] = value['@language']; } if('@direction' in value) { // alias @direction rval[api.compactIri({ activeCtx, iri: '@direction', relativeTo: {vocab: true} })] = value['@direction']; } // alias @value rval[api.compactIri({ activeCtx, iri: '@value', relativeTo: {vocab: true} })] = value['@value']; return rval; } // value is a subject reference const expandedProperty = _expandIri(activeCtx, activeProperty, {vocab: true}, options); const type = _getContextValue(activeCtx, activeProperty, '@type'); const compacted = api.compactIri({ activeCtx, iri: value['@id'], relativeTo: {vocab: type === '@vocab'}, base: options.base}); // compact to scalar if(type === '@id' || type === '@vocab' || expandedProperty === '@graph') { return compacted; } return { [api.compactIri({ activeCtx, iri: '@id', relativeTo: {vocab: true} })]: compacted }; }; /** * Picks the preferred compaction term from the given inverse context entry. * * @param activeCtx the active context. * @param iri the IRI to pick the term for. * @param value the value to pick the term for. * @param containers the preferred containers. * @param typeOrLanguage either '@type' or '@language'. * @param typeOrLanguageValue the preferred value for '@type' or '@language'. * * @return the preferred term. */ function _selectTerm( activeCtx, iri, value, containers, typeOrLanguage, typeOrLanguageValue) { if(typeOrLanguageValue === null) { typeOrLanguageValue = '@null'; } // preferences for the value of @type or @language const prefs = []; // determine prefs for @id based on whether or not value compacts to a term if((typeOrLanguageValue === '@id' || typeOrLanguageValue === '@reverse') && _isObject(value) && '@id' in value) { // prefer @reverse first if(typeOrLanguageValue === '@reverse') { prefs.push('@reverse'); } // try to compact value to a term const term = api.compactIri( {activeCtx, iri: value['@id'], relativeTo: {vocab: true}}); if(activeCtx.mappings.has(term) && activeCtx.mappings.get(term) && activeCtx.mappings.get(term)['@id'] === value['@id']) { // prefer @vocab prefs.push.apply(prefs, ['@vocab', '@id']); } else { // prefer @id prefs.push.apply(prefs, ['@id', '@vocab']); } } else { prefs.push(typeOrLanguageValue); // consider direction only const langDir = prefs.find(el => el.includes('_')); if(langDir) { // consider _dir portion prefs.push(langDir.replace(/^[^_]+_/, '_')); } } prefs.push('@none'); const containerMap = activeCtx.inverse[iri]; for(const container of containers) { // if container not available in the map, continue if(!(container in containerMap)) { continue; } const typeOrLanguageValueMap = containerMap[container][typeOrLanguage]; for(const pref of prefs) { // if type/language option not available in the map, continue if(!(pref in typeOrLanguageValueMap)) { continue; } // select term return typeOrLanguageValueMap[pref]; } } return null; } /** * The value of `@nest` in the term definition must either be `@nest`, or a term * which resolves to `@nest`. * * @param activeCtx the active context. * @param nestProperty a term in the active context or `@nest`. * @param {Object} [options] - processing options. */ function _checkNestProperty(activeCtx, nestProperty, options) { if(_expandIri(activeCtx, nestProperty, {vocab: true}, options) !== '@nest') { throw new JsonLdError( 'JSON-LD compact error; nested property must have an @nest value ' + 'resolving to @nest.', 'jsonld.SyntaxError', {code: 'invalid @nest value'}); } } jsonld.js-4.0.1/lib/constants.js000066400000000000000000000015441401135163200165400ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const RDF = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; const XSD = 'http://www.w3.org/2001/XMLSchema#'; module.exports = { // TODO: Deprecated and will be removed later. Use LINK_HEADER_CONTEXT. LINK_HEADER_REL: 'http://www.w3.org/ns/json-ld#context', LINK_HEADER_CONTEXT: 'http://www.w3.org/ns/json-ld#context', RDF, RDF_LIST: RDF + 'List', RDF_FIRST: RDF + 'first', RDF_REST: RDF + 'rest', RDF_NIL: RDF + 'nil', RDF_TYPE: RDF + 'type', RDF_PLAIN_LITERAL: RDF + 'PlainLiteral', RDF_XML_LITERAL: RDF + 'XMLLiteral', RDF_JSON_LITERAL: RDF + 'JSON', RDF_OBJECT: RDF + 'object', RDF_LANGSTRING: RDF + 'langString', XSD, XSD_BOOLEAN: XSD + 'boolean', XSD_DOUBLE: XSD + 'double', XSD_INTEGER: XSD + 'integer', XSD_STRING: XSD + 'string', }; jsonld.js-4.0.1/lib/context.js000066400000000000000000001341211401135163200162060ustar00rootroot00000000000000/* * Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const util = require('./util'); const JsonLdError = require('./JsonLdError'); const { isArray: _isArray, isObject: _isObject, isString: _isString, isUndefined: _isUndefined } = require('./types'); const { isAbsolute: _isAbsoluteIri, isRelative: _isRelativeIri, prependBase } = require('./url'); const { asArray: _asArray, compareShortestLeast: _compareShortestLeast } = require('./util'); const INITIAL_CONTEXT_CACHE = new Map(); const INITIAL_CONTEXT_CACHE_MAX_SIZE = 10000; const KEYWORD_PATTERN = /^@[a-zA-Z]+$/; const api = {}; module.exports = api; /** * Processes a local context and returns a new active context. * * @param activeCtx the current active context. * @param localCtx the local context to process. * @param options the context processing options. * @param propagate `true` if `false`, retains any previously defined term, * which can be rolled back when the descending into a new node object. * @param overrideProtected `false` allows protected terms to be modified. * * @return a Promise that resolves to the new active context. */ api.process = async ({ activeCtx, localCtx, options, propagate = true, overrideProtected = false, cycles = new Set() }) => { // normalize local context to an array of @context objects if(_isObject(localCtx) && '@context' in localCtx && _isArray(localCtx['@context'])) { localCtx = localCtx['@context']; } const ctxs = _asArray(localCtx); // no contexts in array, return current active context w/o changes if(ctxs.length === 0) { return activeCtx; } // resolve contexts const resolved = await options.contextResolver.resolve({ activeCtx, context: localCtx, documentLoader: options.documentLoader, base: options.base }); // override propagate if first resolved context has `@propagate` if(_isObject(resolved[0].document) && typeof resolved[0].document['@propagate'] === 'boolean') { // retrieve early, error checking done later propagate = resolved[0].document['@propagate']; } // process each context in order, update active context // on each iteration to ensure proper caching let rval = activeCtx; // track the previous context // if not propagating, make sure rval has a previous context if(!propagate && !rval.previousContext) { // clone `rval` context before updating rval = rval.clone(); rval.previousContext = activeCtx; } for(const resolvedContext of resolved) { let {document: ctx} = resolvedContext; // update active context to one computed from last iteration activeCtx = rval; // reset to initial context if(ctx === null) { // We can't nullify if there are protected terms and we're // not allowing overrides (e.g. processing a property term scoped context) if(!overrideProtected && Object.keys(activeCtx.protected).length !== 0) { const protectedMode = (options && options.protectedMode) || 'error'; if(protectedMode === 'error') { throw new JsonLdError( 'Tried to nullify a context with protected terms outside of ' + 'a term definition.', 'jsonld.SyntaxError', {code: 'invalid context nullification'}); } else if(protectedMode === 'warn') { // FIXME: remove logging and use a handler console.warn('WARNING: invalid context nullification'); // get processed context from cache if available const processed = resolvedContext.getProcessed(activeCtx); if(processed) { rval = activeCtx = processed; continue; } const oldActiveCtx = activeCtx; // copy all protected term definitions to fresh initial context rval = activeCtx = api.getInitialContext(options).clone(); for(const [term, _protected] of Object.entries(oldActiveCtx.protected)) { if(_protected) { activeCtx.mappings[term] = util.clone(oldActiveCtx.mappings[term]); } } activeCtx.protected = util.clone(oldActiveCtx.protected); // cache processed result resolvedContext.setProcessed(oldActiveCtx, rval); continue; } throw new JsonLdError( 'Invalid protectedMode.', 'jsonld.SyntaxError', {code: 'invalid protected mode', context: localCtx, protectedMode}); } rval = activeCtx = api.getInitialContext(options).clone(); continue; } // get processed context from cache if available const processed = resolvedContext.getProcessed(activeCtx); if(processed) { rval = activeCtx = processed; continue; } // dereference @context key if present if(_isObject(ctx) && '@context' in ctx) { ctx = ctx['@context']; } // context must be an object by now, all URLs retrieved before this call if(!_isObject(ctx)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context must be an object.', 'jsonld.SyntaxError', {code: 'invalid local context', context: ctx}); } // TODO: there is likely a `previousContext` cloning optimization that // could be applied here (no need to copy it under certain conditions) // clone context before updating it rval = rval.clone(); // define context mappings for keys in local context const defined = new Map(); // handle @version if('@version' in ctx) { if(ctx['@version'] !== 1.1) { throw new JsonLdError( 'Unsupported JSON-LD version: ' + ctx['@version'], 'jsonld.UnsupportedVersion', {code: 'invalid @version value', context: ctx}); } if(activeCtx.processingMode && activeCtx.processingMode === 'json-ld-1.0') { throw new JsonLdError( '@version: ' + ctx['@version'] + ' not compatible with ' + activeCtx.processingMode, 'jsonld.ProcessingModeConflict', {code: 'processing mode conflict', context: ctx}); } rval.processingMode = 'json-ld-1.1'; rval['@version'] = ctx['@version']; defined.set('@version', true); } // if not set explicitly, set processingMode to "json-ld-1.1" rval.processingMode = rval.processingMode || activeCtx.processingMode; // handle @base if('@base' in ctx) { let base = ctx['@base']; if(base === null || _isAbsoluteIri(base)) { // no action } else if(_isRelativeIri(base)) { base = prependBase(rval['@base'], base); } else { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@base" in a ' + '@context must be an absolute IRI, a relative IRI, or null.', 'jsonld.SyntaxError', {code: 'invalid base IRI', context: ctx}); } rval['@base'] = base; defined.set('@base', true); } // handle @vocab if('@vocab' in ctx) { const value = ctx['@vocab']; if(value === null) { delete rval['@vocab']; } else if(!_isString(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@vocab" in a ' + '@context must be a string or null.', 'jsonld.SyntaxError', {code: 'invalid vocab mapping', context: ctx}); } else if(!_isAbsoluteIri(value) && api.processingMode(rval, 1.0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@vocab" in a ' + '@context must be an absolute IRI.', 'jsonld.SyntaxError', {code: 'invalid vocab mapping', context: ctx}); } else { rval['@vocab'] = _expandIri(rval, value, {vocab: true, base: true}, undefined, undefined, options); } defined.set('@vocab', true); } // handle @language if('@language' in ctx) { const value = ctx['@language']; if(value === null) { delete rval['@language']; } else if(!_isString(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@language" in a ' + '@context must be a string or null.', 'jsonld.SyntaxError', {code: 'invalid default language', context: ctx}); } else { rval['@language'] = value.toLowerCase(); } defined.set('@language', true); } // handle @direction if('@direction' in ctx) { const value = ctx['@direction']; if(activeCtx.processingMode === 'json-ld-1.0') { throw new JsonLdError( 'Invalid JSON-LD syntax; @direction not compatible with ' + activeCtx.processingMode, 'jsonld.SyntaxError', {code: 'invalid context member', context: ctx}); } if(value === null) { delete rval['@direction']; } else if(value !== 'ltr' && value !== 'rtl') { throw new JsonLdError( 'Invalid JSON-LD syntax; the value of "@direction" in a ' + '@context must be null, "ltr", or "rtl".', 'jsonld.SyntaxError', {code: 'invalid base direction', context: ctx}); } else { rval['@direction'] = value; } defined.set('@direction', true); } // handle @propagate // note: we've already extracted it, here we just do error checking if('@propagate' in ctx) { const value = ctx['@propagate']; if(activeCtx.processingMode === 'json-ld-1.0') { throw new JsonLdError( 'Invalid JSON-LD syntax; @propagate not compatible with ' + activeCtx.processingMode, 'jsonld.SyntaxError', {code: 'invalid context entry', context: ctx}); } if(typeof value !== 'boolean') { throw new JsonLdError( 'Invalid JSON-LD syntax; @propagate value must be a boolean.', 'jsonld.SyntaxError', {code: 'invalid @propagate value', context: localCtx}); } defined.set('@propagate', true); } // handle @import if('@import' in ctx) { const value = ctx['@import']; if(activeCtx.processingMode === 'json-ld-1.0') { throw new JsonLdError( 'Invalid JSON-LD syntax; @import not compatible with ' + activeCtx.processingMode, 'jsonld.SyntaxError', {code: 'invalid context entry', context: ctx}); } if(!_isString(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @import must be a string.', 'jsonld.SyntaxError', {code: 'invalid @import value', context: localCtx}); } // resolve contexts const resolvedImport = await options.contextResolver.resolve({ activeCtx, context: value, documentLoader: options.documentLoader, base: options.base }); if(resolvedImport.length !== 1) { throw new JsonLdError( 'Invalid JSON-LD syntax; @import must reference a single context.', 'jsonld.SyntaxError', {code: 'invalid remote context', context: localCtx}); } const processedImport = resolvedImport[0].getProcessed(activeCtx); if(processedImport) { // Note: if the same context were used in this active context // as a reference context, then processed_input might not // be a dict. ctx = processedImport; } else { const importCtx = resolvedImport[0].document; if('@import' in importCtx) { throw new JsonLdError( 'Invalid JSON-LD syntax: ' + 'imported context must not include @import.', 'jsonld.SyntaxError', {code: 'invalid context entry', context: localCtx}); } // merge ctx into importCtx and replace rval with the result for(const key in importCtx) { if(!ctx.hasOwnProperty(key)) { ctx[key] = importCtx[key]; } } // Note: this could potenially conflict if the import // were used in the same active context as a referenced // context and an import. In this case, we // could override the cached result, but seems unlikely. resolvedImport[0].setProcessed(activeCtx, ctx); } defined.set('@import', true); } // handle @protected; determine whether this sub-context is declaring // all its terms to be "protected" (exceptions can be made on a // per-definition basis) defined.set('@protected', ctx['@protected'] || false); // process all other keys for(const key in ctx) { api.createTermDefinition({ activeCtx: rval, localCtx: ctx, term: key, defined, options, overrideProtected }); if(_isObject(ctx[key]) && '@context' in ctx[key]) { const keyCtx = ctx[key]['@context']; let process = true; if(_isString(keyCtx)) { const url = prependBase(options.base, keyCtx); // track processed contexts to avoid scoped context recursion if(cycles.has(url)) { process = false; } else { cycles.add(url); } } // parse context to validate if(process) { try { await api.process({ activeCtx: rval.clone(), localCtx: ctx[key]['@context'], overrideProtected: true, options, cycles }); } catch(e) { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid scoped context.', 'jsonld.SyntaxError', { code: 'invalid scoped context', context: ctx[key]['@context'], term: key }); } } } } // cache processed result resolvedContext.setProcessed(activeCtx, rval); } return rval; }; /** * Creates a term definition during context processing. * * @param activeCtx the current active context. * @param localCtx the local context being processed. * @param term the term in the local context to define the mapping for. * @param defined a map of defining/defined keys to detect cycles and prevent * double definitions. * @param {Object} [options] - creation options. * @param {string} [options.protectedMode="error"] - "error" to throw error * on `@protected` constraint violation, "warn" to allow violations and * signal a warning. * @param overrideProtected `false` allows protected terms to be modified. */ api.createTermDefinition = ({ activeCtx, localCtx, term, defined, options, overrideProtected = false, }) => { if(defined.has(term)) { // term already defined if(defined.get(term)) { return; } // cycle detected throw new JsonLdError( 'Cyclical context definition detected.', 'jsonld.CyclicalContext', {code: 'cyclic IRI mapping', context: localCtx, term}); } // now defining term defined.set(term, false); // get context term value let value; if(localCtx.hasOwnProperty(term)) { value = localCtx[term]; } if(term === '@type' && _isObject(value) && (value['@container'] || '@set') === '@set' && api.processingMode(activeCtx, 1.1)) { const validKeys = ['@container', '@id', '@protected']; const keys = Object.keys(value); if(keys.length === 0 || keys.some(k => !validKeys.includes(k))) { throw new JsonLdError( 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); } } else if(api.isKeyword(term)) { throw new JsonLdError( 'Invalid JSON-LD syntax; keywords cannot be overridden.', 'jsonld.SyntaxError', {code: 'keyword redefinition', context: localCtx, term}); } else if(term.match(KEYWORD_PATTERN)) { // FIXME: remove logging and use a handler console.warn('WARNING: terms beginning with "@" are reserved' + ' for future use and ignored', {term}); return; } else if(term === '') { throw new JsonLdError( 'Invalid JSON-LD syntax; a term cannot be an empty string.', 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } // keep reference to previous mapping for potential `@protected` check const previousMapping = activeCtx.mappings.get(term); // remove old mapping if(activeCtx.mappings.has(term)) { activeCtx.mappings.delete(term); } // convert short-hand value to object w/@id let simpleTerm = false; if(_isString(value) || value === null) { simpleTerm = true; value = {'@id': value}; } if(!_isObject(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context term values must be ' + 'strings or objects.', 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } // create new mapping const mapping = {}; activeCtx.mappings.set(term, mapping); mapping.reverse = false; // make sure term definition only has expected keywords const validKeys = ['@container', '@id', '@language', '@reverse', '@type']; // JSON-LD 1.1 support if(api.processingMode(activeCtx, 1.1)) { validKeys.push( '@context', '@direction', '@index', '@nest', '@prefix', '@protected'); } for(const kw in value) { if(!validKeys.includes(kw)) { throw new JsonLdError( 'Invalid JSON-LD syntax; a term definition must not contain ' + kw, 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } } // always compute whether term has a colon as an optimization for // _compactIri const colon = term.indexOf(':'); mapping._termHasColon = (colon > 0); if('@reverse' in value) { if('@id' in value) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @reverse term definition must not ' + 'contain @id.', 'jsonld.SyntaxError', {code: 'invalid reverse property', context: localCtx}); } if('@nest' in value) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @reverse term definition must not ' + 'contain @nest.', 'jsonld.SyntaxError', {code: 'invalid reverse property', context: localCtx}); } const reverse = value['@reverse']; if(!_isString(reverse)) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @context @reverse value must be a string.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } if(!api.isKeyword(reverse) && reverse.match(KEYWORD_PATTERN)) { // FIXME: remove logging and use a handler console.warn('WARNING: values beginning with "@" are reserved' + ' for future use and ignored', {reverse}); if(previousMapping) { activeCtx.mappings.set(term, previousMapping); } else { activeCtx.mappings.delete(term); } return; } // expand and add @id mapping const id = _expandIri( activeCtx, reverse, {vocab: true, base: false}, localCtx, defined, options); if(!_isAbsoluteIri(id)) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @context @reverse value must be an ' + 'absolute IRI or a blank node identifier.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } mapping['@id'] = id; mapping.reverse = true; } else if('@id' in value) { let id = value['@id']; if(id && !_isString(id)) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @context @id value must be an array ' + 'of strings or a string.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } if(id === null) { // reserve a null term, which may be protected mapping['@id'] = null; } else if(!api.isKeyword(id) && id.match(KEYWORD_PATTERN)) { // FIXME: remove logging and use a handler console.warn('WARNING: values beginning with "@" are reserved' + ' for future use and ignored', {id}); if(previousMapping) { activeCtx.mappings.set(term, previousMapping); } else { activeCtx.mappings.delete(term); } return; } else if(id !== term) { // expand and add @id mapping id = _expandIri( activeCtx, id, {vocab: true, base: false}, localCtx, defined, options); if(!_isAbsoluteIri(id) && !api.isKeyword(id)) { throw new JsonLdError( 'Invalid JSON-LD syntax; a @context @id value must be an ' + 'absolute IRI, a blank node identifier, or a keyword.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } // if term has the form of an IRI it must map the same if(term.match(/(?::[^:])|\//)) { const termDefined = new Map(defined).set(term, true); const termIri = _expandIri( activeCtx, term, {vocab: true, base: false}, localCtx, termDefined, options); if(termIri !== id) { throw new JsonLdError( 'Invalid JSON-LD syntax; term in form of IRI must ' + 'expand to definition.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx}); } } mapping['@id'] = id; // indicate if this term may be used as a compact IRI prefix mapping._prefix = (simpleTerm && !mapping._termHasColon && id.match(/[:\/\?#\[\]@]$/)); } } if(!('@id' in mapping)) { // see if the term has a prefix if(mapping._termHasColon) { const prefix = term.substr(0, colon); if(localCtx.hasOwnProperty(prefix)) { // define parent prefix api.createTermDefinition({ activeCtx, localCtx, term: prefix, defined, options }); } if(activeCtx.mappings.has(prefix)) { // set @id based on prefix parent const suffix = term.substr(colon + 1); mapping['@id'] = activeCtx.mappings.get(prefix)['@id'] + suffix; } else { // term is an absolute IRI mapping['@id'] = term; } } else if(term === '@type') { // Special case, were we've previously determined that container is @set mapping['@id'] = term; } else { // non-IRIs *must* define @ids if @vocab is not available if(!('@vocab' in activeCtx)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context terms must define an @id.', 'jsonld.SyntaxError', {code: 'invalid IRI mapping', context: localCtx, term}); } // prepend vocab to term mapping['@id'] = activeCtx['@vocab'] + term; } } // Handle term protection if(value['@protected'] === true || (defined.get('@protected') === true && value['@protected'] !== false)) { activeCtx.protected[term] = true; mapping.protected = true; } // IRI mapping now defined defined.set(term, true); if('@type' in value) { let type = value['@type']; if(!_isString(type)) { throw new JsonLdError( 'Invalid JSON-LD syntax; an @context @type value must be a string.', 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } if((type === '@json' || type === '@none')) { if(api.processingMode(activeCtx, 1.0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; an @context @type value must not be ' + `"${type}" in JSON-LD 1.0 mode.`, 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } } else if(type !== '@id' && type !== '@vocab') { // expand @type to full IRI type = _expandIri( activeCtx, type, {vocab: true, base: false}, localCtx, defined, options); if(!_isAbsoluteIri(type)) { throw new JsonLdError( 'Invalid JSON-LD syntax; an @context @type value must be an ' + 'absolute IRI.', 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } if(type.indexOf('_:') === 0) { throw new JsonLdError( 'Invalid JSON-LD syntax; an @context @type value must be an IRI, ' + 'not a blank node identifier.', 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } } // add @type to mapping mapping['@type'] = type; } if('@container' in value) { // normalize container to an array form const container = _isString(value['@container']) ? [value['@container']] : (value['@container'] || []); const validContainers = ['@list', '@set', '@index', '@language']; let isValid = true; const hasSet = container.includes('@set'); // JSON-LD 1.1 support if(api.processingMode(activeCtx, 1.1)) { validContainers.push('@graph', '@id', '@type'); // check container length if(container.includes('@list')) { if(container.length !== 1) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @container with @list must ' + 'have no other values', 'jsonld.SyntaxError', {code: 'invalid container mapping', context: localCtx}); } } else if(container.includes('@graph')) { if(container.some(key => key !== '@graph' && key !== '@id' && key !== '@index' && key !== '@set')) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @container with @graph must ' + 'have no other values other than @id, @index, and @set', 'jsonld.SyntaxError', {code: 'invalid container mapping', context: localCtx}); } } else { // otherwise, container may also include @set isValid &= container.length <= (hasSet ? 2 : 1); } if(container.includes('@type')) { // If mapping does not have an @type, // set it to @id mapping['@type'] = mapping['@type'] || '@id'; // type mapping must be either @id or @vocab if(!['@id', '@vocab'].includes(mapping['@type'])) { throw new JsonLdError( 'Invalid JSON-LD syntax; container: @type requires @type to be ' + '@id or @vocab.', 'jsonld.SyntaxError', {code: 'invalid type mapping', context: localCtx}); } } } else { // in JSON-LD 1.0, container must not be an array (it must be a string, // which is one of the validContainers) isValid &= !_isArray(value['@container']); // check container length isValid &= container.length <= 1; } // check against valid containers isValid &= container.every(c => validContainers.includes(c)); // @set not allowed with @list isValid &= !(hasSet && container.includes('@list')); if(!isValid) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @container value must be ' + 'one of the following: ' + validContainers.join(', '), 'jsonld.SyntaxError', {code: 'invalid container mapping', context: localCtx}); } if(mapping.reverse && !container.every(c => ['@index', '@set'].includes(c))) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @container value for a @reverse ' + 'type definition must be @index or @set.', 'jsonld.SyntaxError', {code: 'invalid reverse property', context: localCtx}); } // add @container to mapping mapping['@container'] = container; } // property indexing if('@index' in value) { if(!('@container' in value) || !mapping['@container'].includes('@index')) { throw new JsonLdError( 'Invalid JSON-LD syntax; @index without @index in @container: ' + `"${value['@index']}" on term "${term}".`, 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } if(!_isString(value['@index']) || value['@index'].indexOf('@') === 0) { throw new JsonLdError( 'Invalid JSON-LD syntax; @index must expand to an IRI: ' + `"${value['@index']}" on term "${term}".`, 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } mapping['@index'] = value['@index']; } // scoped contexts if('@context' in value) { mapping['@context'] = value['@context']; } if('@language' in value && !('@type' in value)) { let language = value['@language']; if(language !== null && !_isString(language)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @language value must be ' + 'a string or null.', 'jsonld.SyntaxError', {code: 'invalid language mapping', context: localCtx}); } // add @language to mapping if(language !== null) { language = language.toLowerCase(); } mapping['@language'] = language; } // term may be used as a prefix if('@prefix' in value) { if(term.match(/:|\//)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @prefix used on a compact IRI term', 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } if(api.isKeyword(mapping['@id'])) { throw new JsonLdError( 'Invalid JSON-LD syntax; keywords may not be used as prefixes', 'jsonld.SyntaxError', {code: 'invalid term definition', context: localCtx}); } if(typeof value['@prefix'] === 'boolean') { mapping._prefix = value['@prefix'] === true; } else { throw new JsonLdError( 'Invalid JSON-LD syntax; @context value for @prefix must be boolean', 'jsonld.SyntaxError', {code: 'invalid @prefix value', context: localCtx}); } } if('@direction' in value) { const direction = value['@direction']; if(direction !== null && direction !== 'ltr' && direction !== 'rtl') { throw new JsonLdError( 'Invalid JSON-LD syntax; @direction value must be ' + 'null, "ltr", or "rtl".', 'jsonld.SyntaxError', {code: 'invalid base direction', context: localCtx}); } mapping['@direction'] = direction; } if('@nest' in value) { const nest = value['@nest']; if(!_isString(nest) || (nest !== '@nest' && nest.indexOf('@') === 0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; @context @nest value must be ' + 'a string which is not a keyword other than @nest.', 'jsonld.SyntaxError', {code: 'invalid @nest value', context: localCtx}); } mapping['@nest'] = nest; } // disallow aliasing @context and @preserve const id = mapping['@id']; if(id === '@context' || id === '@preserve') { throw new JsonLdError( 'Invalid JSON-LD syntax; @context and @preserve cannot be aliased.', 'jsonld.SyntaxError', {code: 'invalid keyword alias', context: localCtx}); } // Check for overriding protected terms if(previousMapping && previousMapping.protected && !overrideProtected) { // force new term to continue to be protected and see if the mappings would // be equal activeCtx.protected[term] = true; mapping.protected = true; if(!_deepCompare(previousMapping, mapping)) { const protectedMode = (options && options.protectedMode) || 'error'; if(protectedMode === 'error') { throw new JsonLdError( `Invalid JSON-LD syntax; tried to redefine "${term}" which is a protected term.`, 'jsonld.SyntaxError', {code: 'protected term redefinition', context: localCtx, term}); } else if(protectedMode === 'warn') { // FIXME: remove logging and use a handler console.warn('WARNING: protected term redefinition', {term}); return; } throw new JsonLdError( 'Invalid protectedMode.', 'jsonld.SyntaxError', {code: 'invalid protected mode', context: localCtx, term, protectedMode}); } } }; /** * Expands a string to a full IRI. The string may be a term, a prefix, a * relative IRI, or an absolute IRI. The associated absolute IRI will be * returned. * * @param activeCtx the current active context. * @param value the string to expand. * @param relativeTo options for how to resolve relative IRIs: * base: true to resolve against the base IRI, false not to. * vocab: true to concatenate after @vocab, false not to. * @param {Object} [options] - processing options. * * @return the expanded value. */ api.expandIri = (activeCtx, value, relativeTo, options) => { return _expandIri(activeCtx, value, relativeTo, undefined, undefined, options); }; /** * Expands a string to a full IRI. The string may be a term, a prefix, a * relative IRI, or an absolute IRI. The associated absolute IRI will be * returned. * * @param activeCtx the current active context. * @param value the string to expand. * @param relativeTo options for how to resolve relative IRIs: * base: true to resolve against the base IRI, false not to. * vocab: true to concatenate after @vocab, false not to. * @param localCtx the local context being processed (only given if called * during context processing). * @param defined a map for tracking cycles in context definitions (only given * if called during context processing). * @param {Object} [options] - processing options. * * @return the expanded value. */ function _expandIri(activeCtx, value, relativeTo, localCtx, defined, options) { // already expanded if(value === null || !_isString(value) || api.isKeyword(value)) { return value; } // ignore non-keyword things that look like a keyword if(value.match(KEYWORD_PATTERN)) { return null; } // define term dependency if not defined if(localCtx && localCtx.hasOwnProperty(value) && defined.get(value) !== true) { api.createTermDefinition({ activeCtx, localCtx, term: value, defined, options }); } relativeTo = relativeTo || {}; if(relativeTo.vocab) { const mapping = activeCtx.mappings.get(value); // value is explicitly ignored with a null mapping if(mapping === null) { return null; } if(_isObject(mapping) && '@id' in mapping) { // value is a term return mapping['@id']; } } // split value into prefix:suffix const colon = value.indexOf(':'); if(colon > 0) { const prefix = value.substr(0, colon); const suffix = value.substr(colon + 1); // do not expand blank nodes (prefix of '_') or already-absolute // IRIs (suffix of '//') if(prefix === '_' || suffix.indexOf('//') === 0) { return value; } // prefix dependency not defined, define it if(localCtx && localCtx.hasOwnProperty(prefix)) { api.createTermDefinition({ activeCtx, localCtx, term: prefix, defined, options }); } // use mapping if prefix is defined const mapping = activeCtx.mappings.get(prefix); if(mapping && mapping._prefix) { return mapping['@id'] + suffix; } // already absolute IRI if(_isAbsoluteIri(value)) { return value; } } // prepend vocab if(relativeTo.vocab && '@vocab' in activeCtx) { return activeCtx['@vocab'] + value; } // prepend base if(relativeTo.base && '@base' in activeCtx) { if(activeCtx['@base']) { // The null case preserves value as potentially relative return prependBase(prependBase(options.base, activeCtx['@base']), value); } } else if(relativeTo.base) { return prependBase(options.base, value); } return value; } /** * Gets the initial context. * * @param options the options to use: * [base] the document base IRI. * * @return the initial context. */ api.getInitialContext = options => { const key = JSON.stringify({processingMode: options.processingMode}); const cached = INITIAL_CONTEXT_CACHE.get(key); if(cached) { return cached; } const initialContext = { processingMode: options.processingMode, mappings: new Map(), inverse: null, getInverse: _createInverseContext, clone: _cloneActiveContext, revertToPreviousContext: _revertToPreviousContext, protected: {} }; // TODO: consider using LRU cache instead if(INITIAL_CONTEXT_CACHE.size === INITIAL_CONTEXT_CACHE_MAX_SIZE) { // clear whole cache -- assumes scenario where the cache fills means // the cache isn't being used very efficiently anyway INITIAL_CONTEXT_CACHE.clear(); } INITIAL_CONTEXT_CACHE.set(key, initialContext); return initialContext; /** * Generates an inverse context for use in the compaction algorithm, if * not already generated for the given active context. * * @return the inverse context. */ function _createInverseContext() { const activeCtx = this; // lazily create inverse if(activeCtx.inverse) { return activeCtx.inverse; } const inverse = activeCtx.inverse = {}; // variables for building fast CURIE map const fastCurieMap = activeCtx.fastCurieMap = {}; const irisToTerms = {}; // handle default language const defaultLanguage = (activeCtx['@language'] || '@none').toLowerCase(); // handle default direction const defaultDirection = activeCtx['@direction']; // create term selections for each mapping in the context, ordered by // shortest and then lexicographically least const mappings = activeCtx.mappings; const terms = [...mappings.keys()].sort(_compareShortestLeast); for(const term of terms) { const mapping = mappings.get(term); if(mapping === null) { continue; } let container = mapping['@container'] || '@none'; container = [].concat(container).sort().join(''); if(mapping['@id'] === null) { continue; } // iterate over every IRI in the mapping const ids = _asArray(mapping['@id']); for(const iri of ids) { let entry = inverse[iri]; const isKeyword = api.isKeyword(iri); if(!entry) { // initialize entry inverse[iri] = entry = {}; if(!isKeyword && !mapping._termHasColon) { // init IRI to term map and fast CURIE prefixes irisToTerms[iri] = [term]; const fastCurieEntry = {iri, terms: irisToTerms[iri]}; if(iri[0] in fastCurieMap) { fastCurieMap[iri[0]].push(fastCurieEntry); } else { fastCurieMap[iri[0]] = [fastCurieEntry]; } } } else if(!isKeyword && !mapping._termHasColon) { // add IRI to term match irisToTerms[iri].push(term); } // add new entry if(!entry[container]) { entry[container] = { '@language': {}, '@type': {}, '@any': {} }; } entry = entry[container]; _addPreferredTerm(term, entry['@any'], '@none'); if(mapping.reverse) { // term is preferred for values using @reverse _addPreferredTerm(term, entry['@type'], '@reverse'); } else if(mapping['@type'] === '@none') { _addPreferredTerm(term, entry['@any'], '@none'); _addPreferredTerm(term, entry['@language'], '@none'); _addPreferredTerm(term, entry['@type'], '@none'); } else if('@type' in mapping) { // term is preferred for values using specific type _addPreferredTerm(term, entry['@type'], mapping['@type']); } else if('@language' in mapping && '@direction' in mapping) { // term is preferred for values using specific language and direction const language = mapping['@language']; const direction = mapping['@direction']; if(language && direction) { _addPreferredTerm(term, entry['@language'], `${language}_${direction}`.toLowerCase()); } else if(language) { _addPreferredTerm(term, entry['@language'], language.toLowerCase()); } else if(direction) { _addPreferredTerm(term, entry['@language'], `_${direction}`); } else { _addPreferredTerm(term, entry['@language'], '@null'); } } else if('@language' in mapping) { _addPreferredTerm(term, entry['@language'], (mapping['@language'] || '@null').toLowerCase()); } else if('@direction' in mapping) { if(mapping['@direction']) { _addPreferredTerm(term, entry['@language'], `_${mapping['@direction']}`); } else { _addPreferredTerm(term, entry['@language'], '@none'); } } else if(defaultDirection) { _addPreferredTerm(term, entry['@language'], `_${defaultDirection}`); _addPreferredTerm(term, entry['@language'], '@none'); _addPreferredTerm(term, entry['@type'], '@none'); } else { // add entries for no type and no language _addPreferredTerm(term, entry['@language'], defaultLanguage); _addPreferredTerm(term, entry['@language'], '@none'); _addPreferredTerm(term, entry['@type'], '@none'); } } } // build fast CURIE map for(const key in fastCurieMap) { _buildIriMap(fastCurieMap, key, 1); } return inverse; } /** * Runs a recursive algorithm to build a lookup map for quickly finding * potential CURIEs. * * @param iriMap the map to build. * @param key the current key in the map to work on. * @param idx the index into the IRI to compare. */ function _buildIriMap(iriMap, key, idx) { const entries = iriMap[key]; const next = iriMap[key] = {}; let iri; let letter; for(const entry of entries) { iri = entry.iri; if(idx >= iri.length) { letter = ''; } else { letter = iri[idx]; } if(letter in next) { next[letter].push(entry); } else { next[letter] = [entry]; } } for(const key in next) { if(key === '') { continue; } _buildIriMap(next, key, idx + 1); } } /** * Adds the term for the given entry if not already added. * * @param term the term to add. * @param entry the inverse context typeOrLanguage entry to add to. * @param typeOrLanguageValue the key in the entry to add to. */ function _addPreferredTerm(term, entry, typeOrLanguageValue) { if(!entry.hasOwnProperty(typeOrLanguageValue)) { entry[typeOrLanguageValue] = term; } } /** * Clones an active context, creating a child active context. * * @return a clone (child) of the active context. */ function _cloneActiveContext() { const child = {}; child.mappings = util.clone(this.mappings); child.clone = this.clone; child.inverse = null; child.getInverse = this.getInverse; child.protected = util.clone(this.protected); if(this.previousContext) { child.previousContext = this.previousContext.clone(); } child.revertToPreviousContext = this.revertToPreviousContext; if('@base' in this) { child['@base'] = this['@base']; } if('@language' in this) { child['@language'] = this['@language']; } if('@vocab' in this) { child['@vocab'] = this['@vocab']; } return child; } /** * Reverts any type-scoped context in this active context to the previous * context. */ function _revertToPreviousContext() { if(!this.previousContext) { return this; } return this.previousContext.clone(); } }; /** * Gets the value for the given active context key and type, null if none is * set or undefined if none is set and type is '@context'. * * @param ctx the active context. * @param key the context key. * @param [type] the type of value to get (eg: '@id', '@type'), if not * specified gets the entire entry for a key, null if not found. * * @return the value, null, or undefined. */ api.getContextValue = (ctx, key, type) => { // invalid key if(key === null) { if(type === '@context') { return undefined; } return null; } // get specific entry information if(ctx.mappings.has(key)) { const entry = ctx.mappings.get(key); if(_isUndefined(type)) { // return whole entry return entry; } if(entry.hasOwnProperty(type)) { // return entry value for type return entry[type]; } } // get default language if(type === '@language' && type in ctx) { return ctx[type]; } // get default direction if(type === '@direction' && type in ctx) { return ctx[type]; } if(type === '@context') { return undefined; } return null; }; /** * Processing Mode check. * * @param activeCtx the current active context. * @param version the string or numeric version to check. * * @return boolean. */ api.processingMode = (activeCtx, version) => { if(version.toString() >= '1.1') { return !activeCtx.processingMode || activeCtx.processingMode >= 'json-ld-' + version.toString(); } else { return activeCtx.processingMode === 'json-ld-1.0'; } }; /** * Returns whether or not the given value is a keyword. * * @param v the value to check. * * @return true if the value is a keyword, false if not. */ api.isKeyword = v => { if(!_isString(v) || v[0] !== '@') { return false; } switch(v) { case '@base': case '@container': case '@context': case '@default': case '@direction': case '@embed': case '@explicit': case '@graph': case '@id': case '@included': case '@index': case '@json': case '@language': case '@list': case '@nest': case '@none': case '@omitDefault': case '@prefix': case '@preserve': case '@protected': case '@requireAll': case '@reverse': case '@set': case '@type': case '@value': case '@version': case '@vocab': return true; } return false; }; function _deepCompare(x1, x2) { // compare `null` or primitive types directly if((!(x1 && typeof x1 === 'object')) || (!(x2 && typeof x2 === 'object'))) { return x1 === x2; } // x1 and x2 are objects (also potentially arrays) const x1Array = Array.isArray(x1); if(x1Array !== Array.isArray(x2)) { return false; } if(x1Array) { if(x1.length !== x2.length) { return false; } for(let i = 0; i < x1.length; ++i) { if(!_deepCompare(x1[i], x2[i])) { return false; } } return true; } // x1 and x2 are non-array objects const k1s = Object.keys(x1); const k2s = Object.keys(x2); if(k1s.length !== k2s.length) { return false; } for(const k1 in x1) { let v1 = x1[k1]; let v2 = x2[k1]; // special case: `@container` can be in any order if(k1 === '@container') { if(Array.isArray(v1) && Array.isArray(v2)) { v1 = v1.slice().sort(); v2 = v2.slice().sort(); } } if(!_deepCompare(v1, v2)) { return false; } } return true; } jsonld.js-4.0.1/lib/documentLoaders/000077500000000000000000000000001401135163200173125ustar00rootroot00000000000000jsonld.js-4.0.1/lib/documentLoaders/node.js000066400000000000000000000125371401135163200206050ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {parseLinkHeader, buildHeaders} = require('../util'); const {LINK_HEADER_CONTEXT} = require('../constants'); const JsonLdError = require('../JsonLdError'); const RequestQueue = require('../RequestQueue'); const {prependBase} = require('../url'); /** * Creates a built-in node document loader. * * @param options the options to use: * secure: require all URLs to use HTTPS. * strictSSL: true to require SSL certificates to be valid, * false not to (default: true). * maxRedirects: the maximum number of redirects to permit, none by * default. * request: the object which will make the request, default is * provided by `https://www.npmjs.com/package/request`. * headers: an object (map) of headers which will be passed as request * headers for the requested document. Accept is not allowed. * * @return the node document loader. */ module.exports = ({ secure, strictSSL = true, maxRedirects = -1, request, headers = {} } = {strictSSL: true, maxRedirects: -1, headers: {}}) => { headers = buildHeaders(headers); // TODO: use `axios` request = request || require('request'); const http = require('http'); const queue = new RequestQueue(); return queue.wrapLoader(function(url) { return loadDocument(url, []); }); async function loadDocument(url, redirects) { if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; only "http" and "https" URLs are ' + 'supported.', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } if(secure && url.indexOf('https') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; secure mode is enabled and ' + 'the URL\'s scheme is not "https".', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } // TODO: disable cache until HTTP caching implemented let doc = null;//cache.get(url); if(doc !== null) { return doc; } let result; let alternate = null; try { result = await _request(request, { url, headers, strictSSL, followRedirect: false }); } catch(e) { throw new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url, cause: e}); } const {res, body} = result; doc = {contextUrl: null, documentUrl: url, document: body || null}; // handle error const statusText = http.STATUS_CODES[res.statusCode]; if(res.statusCode >= 400) { throw new JsonLdError( `URL "${url}" could not be dereferenced: ${statusText}`, 'jsonld.InvalidUrl', { code: 'loading document failed', url, httpStatusCode: res.statusCode }); } // handle Link Header if(res.headers.link && res.headers['content-type'] !== 'application/ld+json') { // only 1 related link header permitted const linkHeaders = parseLinkHeader(res.headers.link); const linkedContext = linkHeaders[LINK_HEADER_CONTEXT]; if(Array.isArray(linkedContext)) { throw new JsonLdError( 'URL could not be dereferenced, it has more than one associated ' + 'HTTP Link Header.', 'jsonld.InvalidUrl', {code: 'multiple context link headers', url}); } if(linkedContext) { doc.contextUrl = linkedContext.target; } // "alternate" link header is a redirect alternate = linkHeaders['alternate']; if(alternate && alternate.type == 'application/ld+json' && !(res.headers['content-type'] || '') .match(/^application\/(\w*\+)?json$/)) { res.headers.location = prependBase(url, alternate.target); } } // handle redirect if((alternate || res.statusCode >= 300 && res.statusCode < 400) && res.headers.location) { if(redirects.length === maxRedirects) { throw new JsonLdError( 'URL could not be dereferenced; there were too many redirects.', 'jsonld.TooManyRedirects', { code: 'loading document failed', url, httpStatusCode: res.statusCode, redirects }); } if(redirects.indexOf(url) !== -1) { throw new JsonLdError( 'URL could not be dereferenced; infinite redirection was detected.', 'jsonld.InfiniteRedirectDetected', { code: 'recursive context inclusion', url, httpStatusCode: res.statusCode, redirects }); } redirects.push(url); return loadDocument(res.headers.location, redirects); } // cache for each redirected URL redirects.push(url); // TODO: disable cache until HTTP caching implemented /* for(let i = 0; i < redirects.length; ++i) { cache.set( redirects[i], {contextUrl: null, documentUrl: redirects[i], document: body}); } */ return doc; } }; function _request(request, options) { return new Promise((resolve, reject) => { request(options, (err, res, body) => { if(err) { reject(err); } else { resolve({res, body}); } }); }); } jsonld.js-4.0.1/lib/documentLoaders/xhr.js000066400000000000000000000072321401135163200204550ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {parseLinkHeader, buildHeaders} = require('../util'); const {LINK_HEADER_CONTEXT} = require('../constants'); const JsonLdError = require('../JsonLdError'); const RequestQueue = require('../RequestQueue'); const {prependBase} = require('../url'); const REGEX_LINK_HEADER = /(^|(\r\n))link:/i; /** * Creates a built-in XMLHttpRequest document loader. * * @param options the options to use: * secure: require all URLs to use HTTPS. * headers: an object (map) of headers which will be passed as request * headers for the requested document. Accept is not allowed. * [xhr]: the XMLHttpRequest API to use. * * @return the XMLHttpRequest document loader. */ module.exports = ({ secure, headers = {}, xhr } = {headers: {}}) => { headers = buildHeaders(headers); const queue = new RequestQueue(); return queue.wrapLoader(loader); async function loader(url) { if(url.indexOf('http:') !== 0 && url.indexOf('https:') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; only "http" and "https" URLs are ' + 'supported.', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } if(secure && url.indexOf('https') !== 0) { throw new JsonLdError( 'URL could not be dereferenced; secure mode is enabled and ' + 'the URL\'s scheme is not "https".', 'jsonld.InvalidUrl', {code: 'loading document failed', url}); } let req; try { req = await _get(xhr, url, headers); } catch(e) { throw new JsonLdError( 'URL could not be dereferenced, an error occurred.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url, cause: e}); } if(req.status >= 400) { throw new JsonLdError( 'URL could not be dereferenced: ' + req.statusText, 'jsonld.LoadDocumentError', { code: 'loading document failed', url, httpStatusCode: req.status }); } let doc = {contextUrl: null, documentUrl: url, document: req.response}; let alternate = null; // handle Link Header (avoid unsafe header warning by existence testing) const contentType = req.getResponseHeader('Content-Type'); let linkHeader; if(REGEX_LINK_HEADER.test(req.getAllResponseHeaders())) { linkHeader = req.getResponseHeader('Link'); } if(linkHeader && contentType !== 'application/ld+json') { // only 1 related link header permitted const linkHeaders = parseLinkHeader(linkHeader); const linkedContext = linkHeaders[LINK_HEADER_CONTEXT]; if(Array.isArray(linkedContext)) { throw new JsonLdError( 'URL could not be dereferenced, it has more than one ' + 'associated HTTP Link Header.', 'jsonld.InvalidUrl', {code: 'multiple context link headers', url}); } if(linkedContext) { doc.contextUrl = linkedContext.target; } // "alternate" link header is a redirect alternate = linkHeaders['alternate']; if(alternate && alternate.type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { doc = await loader(prependBase(url, alternate.target)); } } return doc; } }; function _get(xhr, url, headers) { xhr = xhr || XMLHttpRequest; const req = new xhr(); return new Promise((resolve, reject) => { req.onload = () => resolve(req); req.onerror = err => reject(err); req.open('GET', url, true); for(const k in headers) { req.setRequestHeader(k, headers[k]); } req.send(); }); } jsonld.js-4.0.1/lib/expand.js000066400000000000000000001043631401135163200160060ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const JsonLdError = require('./JsonLdError'); const { isArray: _isArray, isObject: _isObject, isEmptyObject: _isEmptyObject, isString: _isString, isUndefined: _isUndefined } = require('./types'); const { isList: _isList, isValue: _isValue, isGraph: _isGraph, isSubject: _isSubject } = require('./graphTypes'); const { expandIri: _expandIri, getContextValue: _getContextValue, isKeyword: _isKeyword, process: _processContext, processingMode: _processingMode } = require('./context'); const { isAbsolute: _isAbsoluteIri } = require('./url'); const { addValue: _addValue, asArray: _asArray, getValues: _getValues, validateTypeValue: _validateTypeValue } = require('./util'); const api = {}; module.exports = api; const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; /** * Recursively expands an element using the given context. Any context in * the element will be removed. All context URLs must have been retrieved * before calling this method. * * @param activeCtx the context to use. * @param activeProperty the property for the element, null for none. * @param element the element to expand. * @param options the expansion options. * @param insideList true if the element is a list, false if not. * @param insideIndex true if the element is inside an index container, * false if not. * @param typeScopedContext an optional type-scoped active context for * expanding values of nodes that were expressed according to * a type-scoped context. * @param expansionMap(info) a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. * * @return a Promise that resolves to the expanded value. */ api.expand = async ({ activeCtx, activeProperty = null, element, options = {}, insideList = false, insideIndex = false, typeScopedContext = null, expansionMap = () => undefined }) => { // nothing to expand if(element === null || element === undefined) { return null; } // disable framing if activeProperty is @default if(activeProperty === '@default') { options = Object.assign({}, options, {isFrame: false}); } if(!_isArray(element) && !_isObject(element)) { // drop free-floating scalars that are not in lists unless custom mapped if(!insideList && (activeProperty === null || _expandIri(activeCtx, activeProperty, {vocab: true}, options) === '@graph')) { const mapped = await expansionMap({ unmappedValue: element, activeCtx, activeProperty, options, insideList }); if(mapped === undefined) { return null; } return mapped; } // expand element according to value expansion rules return _expandValue({activeCtx, activeProperty, value: element, options}); } // recursively expand array if(_isArray(element)) { let rval = []; const container = _getContextValue( activeCtx, activeProperty, '@container') || []; insideList = insideList || container.includes('@list'); for(let i = 0; i < element.length; ++i) { // expand element let e = await api.expand({ activeCtx, activeProperty, element: element[i], options, expansionMap, insideIndex, typeScopedContext }); if(insideList && _isArray(e)) { e = {'@list': e}; } if(e === null) { e = await expansionMap({ unmappedValue: element[i], activeCtx, activeProperty, parent: element, index: i, options, expandedParent: rval, insideList }); if(e === undefined) { continue; } } if(_isArray(e)) { rval = rval.concat(e); } else { rval.push(e); } } return rval; } // recursively expand object: // first, expand the active property const expandedActiveProperty = _expandIri( activeCtx, activeProperty, {vocab: true}, options); // Get any property-scoped context for activeProperty const propertyScopedCtx = _getContextValue(activeCtx, activeProperty, '@context'); // second, determine if any type-scoped context should be reverted; it // should only be reverted when the following are all true: // 1. `element` is not a value or subject reference // 2. `insideIndex` is false typeScopedContext = typeScopedContext || (activeCtx.previousContext ? activeCtx : null); let keys = Object.keys(element).sort(); let mustRevert = !insideIndex; if(mustRevert && typeScopedContext && keys.length <= 2 && !keys.includes('@context')) { for(const key of keys) { const expandedProperty = _expandIri( typeScopedContext, key, {vocab: true}, options); if(expandedProperty === '@value') { // value found, ensure type-scoped context is used to expand it mustRevert = false; activeCtx = typeScopedContext; break; } if(expandedProperty === '@id' && keys.length === 1) { // subject reference found, do not revert mustRevert = false; break; } } } if(mustRevert) { // revert type scoped context activeCtx = activeCtx.revertToPreviousContext(); } // apply property-scoped context after reverting term-scoped context if(!_isUndefined(propertyScopedCtx)) { activeCtx = await _processContext({ activeCtx, localCtx: propertyScopedCtx, propagate: true, overrideProtected: true, options }); } // if element has a context, process it if('@context' in element) { activeCtx = await _processContext( {activeCtx, localCtx: element['@context'], options}); } // set the type-scoped context to the context on input, for use later typeScopedContext = activeCtx; // Remember the first key found expanding to @type let typeKey = null; // look for scoped contexts on `@type` for(const key of keys) { const expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); if(expandedProperty === '@type') { // set scoped contexts from @type // avoid sorting if possible typeKey = typeKey || key; const value = element[key]; const types = Array.isArray(value) ? (value.length > 1 ? value.slice().sort() : value) : [value]; for(const type of types) { const ctx = _getContextValue(typeScopedContext, type, '@context'); if(!_isUndefined(ctx)) { activeCtx = await _processContext({ activeCtx, localCtx: ctx, options, propagate: false }); } } } } // process each key and value in element, ignoring @nest content let rval = {}; await _expandObject({ activeCtx, activeProperty, expandedActiveProperty, element, expandedParent: rval, options, insideList, typeKey, typeScopedContext, expansionMap}); // get property count on expanded output keys = Object.keys(rval); let count = keys.length; if('@value' in rval) { // @value must only have @language or @type if('@type' in rval && ('@language' in rval || '@direction' in rval)) { throw new JsonLdError( 'Invalid JSON-LD syntax; an element containing "@value" may not ' + 'contain both "@type" and either "@language" or "@direction".', 'jsonld.SyntaxError', {code: 'invalid value object', element: rval}); } let validCount = count - 1; if('@type' in rval) { validCount -= 1; } if('@index' in rval) { validCount -= 1; } if('@language' in rval) { validCount -= 1; } if('@direction' in rval) { validCount -= 1; } if(validCount !== 0) { throw new JsonLdError( 'Invalid JSON-LD syntax; an element containing "@value" may only ' + 'have an "@index" property and either "@type" ' + 'or either or both "@language" or "@direction".', 'jsonld.SyntaxError', {code: 'invalid value object', element: rval}); } const values = rval['@value'] === null ? [] : _asArray(rval['@value']); const types = _getValues(rval, '@type'); // drop null @values unless custom mapped if(_processingMode(activeCtx, 1.1) && types.includes('@json') && types.length === 1) { // Any value of @value is okay if @type: @json } else if(values.length === 0) { const mapped = await expansionMap({ unmappedValue: rval, activeCtx, activeProperty, element, options, insideList }); if(mapped !== undefined) { rval = mapped; } else { rval = null; } } else if(!values.every(v => (_isString(v) || _isEmptyObject(v))) && '@language' in rval) { // if @language is present, @value must be a string throw new JsonLdError( 'Invalid JSON-LD syntax; only strings may be language-tagged.', 'jsonld.SyntaxError', {code: 'invalid language-tagged value', element: rval}); } else if(!types.every(t => (_isAbsoluteIri(t) && !(_isString(t) && t.indexOf('_:') === 0) || _isEmptyObject(t)))) { throw new JsonLdError( 'Invalid JSON-LD syntax; an element containing "@value" and "@type" ' + 'must have an absolute IRI for the value of "@type".', 'jsonld.SyntaxError', {code: 'invalid typed value', element: rval}); } } else if('@type' in rval && !_isArray(rval['@type'])) { // convert @type to an array rval['@type'] = [rval['@type']]; } else if('@set' in rval || '@list' in rval) { // handle @set and @list if(count > 1 && !(count === 2 && '@index' in rval)) { throw new JsonLdError( 'Invalid JSON-LD syntax; if an element has the property "@set" ' + 'or "@list", then it can have at most one other property that is ' + '"@index".', 'jsonld.SyntaxError', {code: 'invalid set or list object', element: rval}); } // optimize away @set if('@set' in rval) { rval = rval['@set']; keys = Object.keys(rval); count = keys.length; } } else if(count === 1 && '@language' in rval) { // drop objects with only @language unless custom mapped const mapped = await expansionMap(rval, { unmappedValue: rval, activeCtx, activeProperty, element, options, insideList }); if(mapped !== undefined) { rval = mapped; } else { rval = null; } } // drop certain top-level objects that do not occur in lists, unless custom // mapped if(_isObject(rval) && !options.keepFreeFloatingNodes && !insideList && (activeProperty === null || expandedActiveProperty === '@graph')) { // drop empty object, top-level @value/@list, or object with only @id if(count === 0 || '@value' in rval || '@list' in rval || (count === 1 && '@id' in rval)) { const mapped = await expansionMap({ unmappedValue: rval, activeCtx, activeProperty, element, options, insideList }); if(mapped !== undefined) { rval = mapped; } else { rval = null; } } } return rval; }; /** * Expand each key and value of element adding to result * * @param activeCtx the context to use. * @param activeProperty the property for the element. * @param expandedActiveProperty the expansion of activeProperty * @param element the element to expand. * @param expandedParent the expanded result into which to add values. * @param options the expansion options. * @param insideList true if the element is a list, false if not. * @param typeKey first key found expanding to @type. * @param typeScopedContext the context before reverting. * @param expansionMap(info) a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. */ async function _expandObject({ activeCtx, activeProperty, expandedActiveProperty, element, expandedParent, options = {}, insideList, typeKey, typeScopedContext, expansionMap }) { const keys = Object.keys(element).sort(); const nests = []; let unexpandedValue; // Figure out if this is the type for a JSON literal const isJsonType = element[typeKey] && _expandIri(activeCtx, (_isArray(element[typeKey]) ? element[typeKey][0] : element[typeKey]), {vocab: true}, options) === '@json'; for(const key of keys) { let value = element[key]; let expandedValue; // skip @context if(key === '@context') { continue; } // expand property let expandedProperty = _expandIri(activeCtx, key, {vocab: true}, options); // drop non-absolute IRI keys that aren't keywords unless custom mapped if(expandedProperty === null || !(_isAbsoluteIri(expandedProperty) || _isKeyword(expandedProperty))) { // TODO: use `await` to support async expandedProperty = expansionMap({ unmappedProperty: key, activeCtx, activeProperty, parent: element, options, insideList, value, expandedParent }); if(expandedProperty === undefined) { continue; } } if(_isKeyword(expandedProperty)) { if(expandedActiveProperty === '@reverse') { throw new JsonLdError( 'Invalid JSON-LD syntax; a keyword cannot be used as a @reverse ' + 'property.', 'jsonld.SyntaxError', {code: 'invalid reverse property map', value}); } if(expandedProperty in expandedParent && expandedProperty !== '@included' && expandedProperty !== '@type') { throw new JsonLdError( 'Invalid JSON-LD syntax; colliding keywords detected.', 'jsonld.SyntaxError', {code: 'colliding keywords', keyword: expandedProperty}); } } // syntax error if @id is not a string if(expandedProperty === '@id') { if(!_isString(value)) { if(!options.isFrame) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@id" value must a string.', 'jsonld.SyntaxError', {code: 'invalid @id value', value}); } if(_isObject(value)) { // empty object is a wildcard if(!_isEmptyObject(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@id" value an empty object or array ' + 'of strings, if framing', 'jsonld.SyntaxError', {code: 'invalid @id value', value}); } } else if(_isArray(value)) { if(!value.every(v => _isString(v))) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@id" value an empty object or array ' + 'of strings, if framing', 'jsonld.SyntaxError', {code: 'invalid @id value', value}); } } else { throw new JsonLdError( 'Invalid JSON-LD syntax; "@id" value an empty object or array ' + 'of strings, if framing', 'jsonld.SyntaxError', {code: 'invalid @id value', value}); } } _addValue( expandedParent, '@id', _asArray(value).map(v => _isString(v) ? _expandIri(activeCtx, v, {base: true}, options) : v), {propertyIsArray: options.isFrame}); continue; } if(expandedProperty === '@type') { // if framing, can be a default object, but need to expand // key to determine that if(_isObject(value)) { value = Object.fromEntries(Object.entries(value).map(([k, v]) => [ _expandIri(typeScopedContext, k, {vocab: true}), _asArray(v).map(vv => _expandIri(typeScopedContext, vv, {base: true, vocab: true}) ) ])); } _validateTypeValue(value, options.isFrame); _addValue( expandedParent, '@type', _asArray(value).map(v => _isString(v) ? _expandIri(typeScopedContext, v, {base: true, vocab: true}, options) : v), {propertyIsArray: options.isFrame}); continue; } // Included blocks are treated as an array of separate object nodes sharing // the same referencing active_property. // For 1.0, it is skipped as are other unknown keywords if(expandedProperty === '@included' && _processingMode(activeCtx, 1.1)) { const includedResult = _asArray(await api.expand({ activeCtx, activeProperty, element: value, options, expansionMap })); // Expanded values must be node objects if(!includedResult.every(v => _isSubject(v))) { throw new JsonLdError( 'Invalid JSON-LD syntax; ' + 'values of @included must expand to node objects.', 'jsonld.SyntaxError', {code: 'invalid @included value', value}); } _addValue( expandedParent, '@included', includedResult, {propertyIsArray: true}); continue; } // @graph must be an array or an object if(expandedProperty === '@graph' && !(_isObject(value) || _isArray(value))) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@graph" value must not be an ' + 'object or an array.', 'jsonld.SyntaxError', {code: 'invalid @graph value', value}); } if(expandedProperty === '@value') { // capture value for later // "colliding keywords" check prevents this from being set twice unexpandedValue = value; if(isJsonType && _processingMode(activeCtx, 1.1)) { // no coercion to array, and retain all values expandedParent['@value'] = value; } else { _addValue( expandedParent, '@value', value, {propertyIsArray: options.isFrame}); } continue; } // @language must be a string // it should match BCP47 if(expandedProperty === '@language') { if(value === null) { // drop null @language values, they expand as if they didn't exist continue; } if(!_isString(value) && !options.isFrame) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@language" value must be a string.', 'jsonld.SyntaxError', {code: 'invalid language-tagged string', value}); } // ensure language value is lowercase value = _asArray(value).map(v => _isString(v) ? v.toLowerCase() : v); // ensure language tag matches BCP47 for(const lang of value) { if(_isString(lang) && !lang.match(REGEX_BCP47)) { console.warn(`@language must be valid BCP47: ${lang}`); } } _addValue( expandedParent, '@language', value, {propertyIsArray: options.isFrame}); continue; } // @direction must be "ltr" or "rtl" if(expandedProperty === '@direction') { if(!_isString(value) && !options.isFrame) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@direction" value must be a string.', 'jsonld.SyntaxError', {code: 'invalid base direction', value}); } value = _asArray(value); // ensure direction is "ltr" or "rtl" for(const dir of value) { if(_isString(dir) && dir !== 'ltr' && dir !== 'rtl') { throw new JsonLdError( 'Invalid JSON-LD syntax; "@direction" must be "ltr" or "rtl".', 'jsonld.SyntaxError', {code: 'invalid base direction', value}); } } _addValue( expandedParent, '@direction', value, {propertyIsArray: options.isFrame}); continue; } // @index must be a string if(expandedProperty === '@index') { if(!_isString(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@index" value must be a string.', 'jsonld.SyntaxError', {code: 'invalid @index value', value}); } _addValue(expandedParent, '@index', value); continue; } // @reverse must be an object if(expandedProperty === '@reverse') { if(!_isObject(value)) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@reverse" value must be an object.', 'jsonld.SyntaxError', {code: 'invalid @reverse value', value}); } expandedValue = await api.expand({ activeCtx, activeProperty: '@reverse', element: value, options, expansionMap }); // properties double-reversed if('@reverse' in expandedValue) { for(const property in expandedValue['@reverse']) { _addValue( expandedParent, property, expandedValue['@reverse'][property], {propertyIsArray: true}); } } // FIXME: can this be merged with code below to simplify? // merge in all reversed properties let reverseMap = expandedParent['@reverse'] || null; for(const property in expandedValue) { if(property === '@reverse') { continue; } if(reverseMap === null) { reverseMap = expandedParent['@reverse'] = {}; } _addValue(reverseMap, property, [], {propertyIsArray: true}); const items = expandedValue[property]; for(let ii = 0; ii < items.length; ++ii) { const item = items[ii]; if(_isValue(item) || _isList(item)) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@reverse" value must not be a ' + '@value or an @list.', 'jsonld.SyntaxError', {code: 'invalid reverse property value', value: expandedValue}); } _addValue(reverseMap, property, item, {propertyIsArray: true}); } } continue; } // nested keys if(expandedProperty === '@nest') { nests.push(key); continue; } // use potential scoped context for key let termCtx = activeCtx; const ctx = _getContextValue(activeCtx, key, '@context'); if(!_isUndefined(ctx)) { termCtx = await _processContext({ activeCtx, localCtx: ctx, propagate: true, overrideProtected: true, options }); } const container = _getContextValue(termCtx, key, '@container') || []; if(container.includes('@language') && _isObject(value)) { const direction = _getContextValue(termCtx, key, '@direction'); // handle language map container (skip if value is not an object) expandedValue = _expandLanguageMap(termCtx, value, direction, options); } else if(container.includes('@index') && _isObject(value)) { // handle index container (skip if value is not an object) const asGraph = container.includes('@graph'); const indexKey = _getContextValue(termCtx, key, '@index') || '@index'; const propertyIndex = indexKey !== '@index' && _expandIri(activeCtx, indexKey, {vocab: true}, options); expandedValue = await _expandIndexMap({ activeCtx: termCtx, options, activeProperty: key, value, expansionMap, asGraph, indexKey, propertyIndex }); } else if(container.includes('@id') && _isObject(value)) { // handle id container (skip if value is not an object) const asGraph = container.includes('@graph'); expandedValue = await _expandIndexMap({ activeCtx: termCtx, options, activeProperty: key, value, expansionMap, asGraph, indexKey: '@id' }); } else if(container.includes('@type') && _isObject(value)) { // handle type container (skip if value is not an object) expandedValue = await _expandIndexMap({ // since container is `@type`, revert type scoped context when expanding activeCtx: termCtx.revertToPreviousContext(), options, activeProperty: key, value, expansionMap, asGraph: false, indexKey: '@type' }); } else { // recurse into @list or @set const isList = (expandedProperty === '@list'); if(isList || expandedProperty === '@set') { let nextActiveProperty = activeProperty; if(isList && expandedActiveProperty === '@graph') { nextActiveProperty = null; } expandedValue = await api.expand({ activeCtx: termCtx, activeProperty: nextActiveProperty, element: value, options, insideList: isList, expansionMap }); } else if( _getContextValue(activeCtx, key, '@type') === '@json') { expandedValue = { '@type': '@json', '@value': value }; } else { // recursively expand value with key as new active property expandedValue = await api.expand({ activeCtx: termCtx, activeProperty: key, element: value, options, insideList: false, expansionMap }); } } // drop null values if property is not @value if(expandedValue === null && expandedProperty !== '@value') { // TODO: use `await` to support async expandedValue = expansionMap({ unmappedValue: value, expandedProperty, activeCtx: termCtx, activeProperty, parent: element, options, insideList, key, expandedParent }); if(expandedValue === undefined) { continue; } } // convert expanded value to @list if container specifies it if(expandedProperty !== '@list' && !_isList(expandedValue) && container.includes('@list')) { // ensure expanded value in @list is an array expandedValue = {'@list': _asArray(expandedValue)}; } // convert expanded value to @graph if container specifies it // and value is not, itself, a graph // index cases handled above if(container.includes('@graph') && !container.some(key => key === '@id' || key === '@index')) { // ensure expanded values are arrays expandedValue = _asArray(expandedValue) .map(v => ({'@graph': _asArray(v)})); } // FIXME: can this be merged with code above to simplify? // merge in reverse properties if(termCtx.mappings.has(key) && termCtx.mappings.get(key).reverse) { const reverseMap = expandedParent['@reverse'] = expandedParent['@reverse'] || {}; expandedValue = _asArray(expandedValue); for(let ii = 0; ii < expandedValue.length; ++ii) { const item = expandedValue[ii]; if(_isValue(item) || _isList(item)) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@reverse" value must not be a ' + '@value or an @list.', 'jsonld.SyntaxError', {code: 'invalid reverse property value', value: expandedValue}); } _addValue(reverseMap, expandedProperty, item, {propertyIsArray: true}); } continue; } // add value for property // special keywords handled above _addValue(expandedParent, expandedProperty, expandedValue, { propertyIsArray: true }); } // @value must not be an object or an array (unless framing) or if @type is // @json if('@value' in expandedParent) { if(expandedParent['@type'] === '@json' && _processingMode(activeCtx, 1.1)) { // allow any value, to be verified when the object is fully expanded and // the @type is @json. } else if((_isObject(unexpandedValue) || _isArray(unexpandedValue)) && !options.isFrame) { throw new JsonLdError( 'Invalid JSON-LD syntax; "@value" value must not be an ' + 'object or an array.', 'jsonld.SyntaxError', {code: 'invalid value object value', value: unexpandedValue}); } } // expand each nested key for(const key of nests) { const nestedValues = _isArray(element[key]) ? element[key] : [element[key]]; for(const nv of nestedValues) { if(!_isObject(nv) || Object.keys(nv).some(k => _expandIri(activeCtx, k, {vocab: true}, options) === '@value')) { throw new JsonLdError( 'Invalid JSON-LD syntax; nested value must be a node object.', 'jsonld.SyntaxError', {code: 'invalid @nest value', value: nv}); } await _expandObject({ activeCtx, activeProperty, expandedActiveProperty, element: nv, expandedParent, options, insideList, typeScopedContext, typeKey, expansionMap}); } } } /** * Expands the given value by using the coercion and keyword rules in the * given context. * * @param activeCtx the active context to use. * @param activeProperty the active property the value is associated with. * @param value the value to expand. * @param {Object} [options] - processing options. * * @return the expanded value. */ function _expandValue({activeCtx, activeProperty, value, options}) { // nothing to expand if(value === null || value === undefined) { return null; } // special-case expand @id and @type (skips '@id' expansion) const expandedProperty = _expandIri( activeCtx, activeProperty, {vocab: true}, options); if(expandedProperty === '@id') { return _expandIri(activeCtx, value, {base: true}, options); } else if(expandedProperty === '@type') { return _expandIri(activeCtx, value, {vocab: true, base: true}, options); } // get type definition from context const type = _getContextValue(activeCtx, activeProperty, '@type'); // do @id expansion (automatic for @graph) if((type === '@id' || expandedProperty === '@graph') && _isString(value)) { return {'@id': _expandIri(activeCtx, value, {base: true}, options)}; } // do @id expansion w/vocab if(type === '@vocab' && _isString(value)) { return { '@id': _expandIri(activeCtx, value, {vocab: true, base: true}, options) }; } // do not expand keyword values if(_isKeyword(expandedProperty)) { return value; } const rval = {}; if(type && !['@id', '@vocab', '@none'].includes(type)) { // other type rval['@type'] = type; } else if(_isString(value)) { // check for language tagging for strings const language = _getContextValue(activeCtx, activeProperty, '@language'); if(language !== null) { rval['@language'] = language; } const direction = _getContextValue(activeCtx, activeProperty, '@direction'); if(direction !== null) { rval['@direction'] = direction; } } // do conversion of values that aren't basic JSON types to strings if(!['boolean', 'number', 'string'].includes(typeof value)) { value = value.toString(); } rval['@value'] = value; return rval; } /** * Expands a language map. * * @param activeCtx the active context to use. * @param languageMap the language map to expand. * @param direction the direction to apply to values. * @param {Object} [options] - processing options. * * @return the expanded language map. */ function _expandLanguageMap(activeCtx, languageMap, direction, options) { const rval = []; const keys = Object.keys(languageMap).sort(); for(const key of keys) { const expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); let val = languageMap[key]; if(!_isArray(val)) { val = [val]; } for(const item of val) { if(item === null) { // null values are allowed (8.5) but ignored (3.1) continue; } if(!_isString(item)) { throw new JsonLdError( 'Invalid JSON-LD syntax; language map values must be strings.', 'jsonld.SyntaxError', {code: 'invalid language map value', languageMap}); } const val = {'@value': item}; if(expandedKey !== '@none') { val['@language'] = key.toLowerCase(); } if(direction) { val['@direction'] = direction; } rval.push(val); } } return rval; } async function _expandIndexMap( {activeCtx, options, activeProperty, value, expansionMap, asGraph, indexKey, propertyIndex}) { const rval = []; const keys = Object.keys(value).sort(); const isTypeIndex = indexKey === '@type'; for(let key of keys) { // if indexKey is @type, there may be a context defined for it if(isTypeIndex) { const ctx = _getContextValue(activeCtx, key, '@context'); if(!_isUndefined(ctx)) { activeCtx = await _processContext({ activeCtx, localCtx: ctx, propagate: false, options }); } } let val = value[key]; if(!_isArray(val)) { val = [val]; } val = await api.expand({ activeCtx, activeProperty, element: val, options, insideList: false, insideIndex: true, expansionMap }); // expand for @type, but also for @none let expandedKey; if(propertyIndex) { if(key === '@none') { expandedKey = '@none'; } else { expandedKey = _expandValue( {activeCtx, activeProperty: indexKey, value: key, options}); } } else { expandedKey = _expandIri(activeCtx, key, {vocab: true}, options); } if(indexKey === '@id') { // expand document relative key = _expandIri(activeCtx, key, {base: true}, options); } else if(isTypeIndex) { key = expandedKey; } for(let item of val) { // If this is also a @graph container, turn items into graphs if(asGraph && !_isGraph(item)) { item = {'@graph': [item]}; } if(indexKey === '@type') { if(expandedKey === '@none') { // ignore @none } else if(item['@type']) { item['@type'] = [key].concat(item['@type']); } else { item['@type'] = [key]; } } else if(_isValue(item) && !['@language', '@type', '@index'].includes(indexKey)) { throw new JsonLdError( 'Invalid JSON-LD syntax; Attempt to add illegal key to value ' + `object: "${indexKey}".`, 'jsonld.SyntaxError', {code: 'invalid value object', value: item}); } else if(propertyIndex) { // index is a property to be expanded, and values interpreted for that // property if(expandedKey !== '@none') { // expand key as a value _addValue(item, propertyIndex, expandedKey, { propertyIsArray: true, prependValue: true }); } } else if(expandedKey !== '@none' && !(indexKey in item)) { item[indexKey] = key; } rval.push(item); } } return rval; } jsonld.js-4.0.1/lib/flatten.js000066400000000000000000000014631401135163200161610ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const { isSubjectReference: _isSubjectReference } = require('./graphTypes'); const { createMergedNodeMap: _createMergedNodeMap } = require('./nodeMap'); const api = {}; module.exports = api; /** * Performs JSON-LD flattening. * * @param input the expanded JSON-LD to flatten. * * @return the flattened output. */ api.flatten = input => { const defaultGraph = _createMergedNodeMap(input); // produce flattened output const flattened = []; const keys = Object.keys(defaultGraph).sort(); for(let ki = 0; ki < keys.length; ++ki) { const node = defaultGraph[keys[ki]]; // only add full subjects to top-level if(!_isSubjectReference(node)) { flattened.push(node); } } return flattened; }; jsonld.js-4.0.1/lib/frame.js000066400000000000000000000612221401135163200156150ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); const url = require('./url'); const JsonLdError = require('./JsonLdError'); const { createNodeMap: _createNodeMap, mergeNodeMapGraphs: _mergeNodeMapGraphs } = require('./nodeMap'); const api = {}; module.exports = api; /** * Performs JSON-LD `merged` framing. * * @param input the expanded JSON-LD to frame. * @param frame the expanded JSON-LD frame to use. * @param options the framing options. * * @return the framed output. */ api.frameMergedOrDefault = (input, frame, options) => { // create framing state const state = { options, embedded: false, graph: '@default', graphMap: {'@default': {}}, subjectStack: [], link: {}, bnodeMap: {} }; // produce a map of all graphs and name each bnode // FIXME: currently uses subjects from @merged graph only const issuer = new util.IdentifierIssuer('_:b'); _createNodeMap(input, state.graphMap, '@default', issuer); if(options.merged) { state.graphMap['@merged'] = _mergeNodeMapGraphs(state.graphMap); state.graph = '@merged'; } state.subjects = state.graphMap[state.graph]; // frame the subjects const framed = []; api.frame(state, Object.keys(state.subjects).sort(), frame, framed); // If pruning blank nodes, find those to prune if(options.pruneBlankNodeIdentifiers) { // remove all blank nodes appearing only once, done in compaction options.bnodesToClear = Object.keys(state.bnodeMap).filter(id => state.bnodeMap[id].length === 1); } // remove @preserve from results options.link = {}; return _cleanupPreserve(framed, options); }; /** * Frames subjects according to the given frame. * * @param state the current framing state. * @param subjects the subjects to filter. * @param frame the frame. * @param parent the parent subject or top-level array. * @param property the parent property, initialized to null. */ api.frame = (state, subjects, frame, parent, property = null) => { // validate the frame _validateFrame(frame); frame = frame[0]; // get flags for current frame const options = state.options; const flags = { embed: _getFrameFlag(frame, options, 'embed'), explicit: _getFrameFlag(frame, options, 'explicit'), requireAll: _getFrameFlag(frame, options, 'requireAll') }; // get link for current graph if(!state.link.hasOwnProperty(state.graph)) { state.link[state.graph] = {}; } const link = state.link[state.graph]; // filter out subjects that match the frame const matches = _filterSubjects(state, subjects, frame, flags); // add matches to output const ids = Object.keys(matches).sort(); for(const id of ids) { const subject = matches[id]; /* Note: In order to treat each top-level match as a compartmentalized result, clear the unique embedded subjects map when the property is null, which only occurs at the top-level. */ if(property === null) { state.uniqueEmbeds = {[state.graph]: {}}; } else { state.uniqueEmbeds[state.graph] = state.uniqueEmbeds[state.graph] || {}; } if(flags.embed === '@link' && id in link) { // TODO: may want to also match an existing linked subject against // the current frame ... so different frames could produce different // subjects that are only shared in-memory when the frames are the same // add existing linked subject _addFrameOutput(parent, property, link[id]); continue; } // start output for subject const output = {'@id': id}; if(id.indexOf('_:') === 0) { util.addValue(state.bnodeMap, id, output, {propertyIsArray: true}); } link[id] = output; // validate @embed if((flags.embed === '@first' || flags.embed === '@last') && state.is11) { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid value of @embed.', 'jsonld.SyntaxError', {code: 'invalid @embed value', frame}); } if(!state.embedded && state.uniqueEmbeds[state.graph].hasOwnProperty(id)) { // skip adding this node object to the top level, as it was // already included in another node object continue; } // if embed is @never or if a circular reference would be created by an // embed, the subject cannot be embedded, just add the reference; // note that a circular reference won't occur when the embed flag is // `@link` as the above check will short-circuit before reaching this point if(state.embedded && (flags.embed === '@never' || _createsCircularReference(subject, state.graph, state.subjectStack))) { _addFrameOutput(parent, property, output); continue; } // if only the first (or once) should be embedded if(state.embedded && (flags.embed == '@first' || flags.embed == '@once') && state.uniqueEmbeds[state.graph].hasOwnProperty(id)) { _addFrameOutput(parent, property, output); continue; } // if only the last match should be embedded if(flags.embed === '@last') { // remove any existing embed if(id in state.uniqueEmbeds[state.graph]) { _removeEmbed(state, id); } } state.uniqueEmbeds[state.graph][id] = {parent, property}; // push matching subject onto stack to enable circular embed checks state.subjectStack.push({subject, graph: state.graph}); // subject is also the name of a graph if(id in state.graphMap) { let recurse = false; let subframe = null; if(!('@graph' in frame)) { recurse = state.graph !== '@merged'; subframe = {}; } else { subframe = frame['@graph'][0]; recurse = !(id === '@merged' || id === '@default'); if(!types.isObject(subframe)) { subframe = {}; } } if(recurse) { // recurse into graph api.frame( {...state, graph: id, embedded: false}, Object.keys(state.graphMap[id]).sort(), [subframe], output, '@graph'); } } // if frame has @included, recurse over its sub-frame if('@included' in frame) { api.frame( {...state, embedded: false}, subjects, frame['@included'], output, '@included'); } // iterate over subject properties for(const prop of Object.keys(subject).sort()) { // copy keywords to output if(isKeyword(prop)) { output[prop] = util.clone(subject[prop]); if(prop === '@type') { // count bnode values of @type for(const type of subject['@type']) { if(type.indexOf('_:') === 0) { util.addValue( state.bnodeMap, type, output, {propertyIsArray: true}); } } } continue; } // explicit is on and property isn't in the frame, skip processing if(flags.explicit && !(prop in frame)) { continue; } // add objects for(const o of subject[prop]) { const subframe = (prop in frame ? frame[prop] : _createImplicitFrame(flags)); // recurse into list if(graphTypes.isList(o)) { const subframe = (frame[prop] && frame[prop][0] && frame[prop][0]['@list']) ? frame[prop][0]['@list'] : _createImplicitFrame(flags); // add empty list const list = {'@list': []}; _addFrameOutput(output, prop, list); // add list objects const src = o['@list']; for(const oo of src) { if(graphTypes.isSubjectReference(oo)) { // recurse into subject reference api.frame( {...state, embedded: true}, [oo['@id']], subframe, list, '@list'); } else { // include other values automatically _addFrameOutput(list, '@list', util.clone(oo)); } } } else if(graphTypes.isSubjectReference(o)) { // recurse into subject reference api.frame( {...state, embedded: true}, [o['@id']], subframe, output, prop); } else if(_valueMatch(subframe[0], o)) { // include other values, if they match _addFrameOutput(output, prop, util.clone(o)); } } } // handle defaults for(const prop of Object.keys(frame).sort()) { // skip keywords if(prop === '@type') { if(!types.isObject(frame[prop][0]) || !('@default' in frame[prop][0])) { continue; } // allow through default types } else if(isKeyword(prop)) { continue; } // if omit default is off, then include default values for properties // that appear in the next frame but are not in the matching subject const next = frame[prop][0] || {}; const omitDefaultOn = _getFrameFlag(next, options, 'omitDefault'); if(!omitDefaultOn && !(prop in output)) { let preserve = '@null'; if('@default' in next) { preserve = util.clone(next['@default']); } if(!types.isArray(preserve)) { preserve = [preserve]; } output[prop] = [{'@preserve': preserve}]; } } // if embed reverse values by finding nodes having this subject as a value // of the associated property for(const reverseProp of Object.keys(frame['@reverse'] || {}).sort()) { const subframe = frame['@reverse'][reverseProp]; for(const subject of Object.keys(state.subjects)) { const nodeValues = util.getValues(state.subjects[subject], reverseProp); if(nodeValues.some(v => v['@id'] === id)) { // node has property referencing this subject, recurse output['@reverse'] = output['@reverse'] || {}; util.addValue( output['@reverse'], reverseProp, [], {propertyIsArray: true}); api.frame( {...state, embedded: true}, [subject], subframe, output['@reverse'][reverseProp], property); } } } // add output to parent _addFrameOutput(parent, property, output); // pop matching subject from circular ref-checking stack state.subjectStack.pop(); } }; /** * Replace `@null` with `null`, removing it from arrays. * * @param input the framed, compacted output. * @param options the framing options used. * * @return the resulting output. */ api.cleanupNull = (input, options) => { // recurse through arrays if(types.isArray(input)) { const noNulls = input.map(v => api.cleanupNull(v, options)); return noNulls.filter(v => v); // removes nulls from array } if(input === '@null') { return null; } if(types.isObject(input)) { // handle in-memory linked nodes if('@id' in input) { const id = input['@id']; if(options.link.hasOwnProperty(id)) { const idx = options.link[id].indexOf(input); if(idx !== -1) { // already visited return options.link[id][idx]; } // prevent circular visitation options.link[id].push(input); } else { // prevent circular visitation options.link[id] = [input]; } } for(const key in input) { input[key] = api.cleanupNull(input[key], options); } } return input; }; /** * Creates an implicit frame when recursing through subject matches. If * a frame doesn't have an explicit frame for a particular property, then * a wildcard child frame will be created that uses the same flags that the * parent frame used. * * @param flags the current framing flags. * * @return the implicit frame. */ function _createImplicitFrame(flags) { const frame = {}; for(const key in flags) { if(flags[key] !== undefined) { frame['@' + key] = [flags[key]]; } } return [frame]; } /** * Checks the current subject stack to see if embedding the given subject * would cause a circular reference. * * @param subjectToEmbed the subject to embed. * @param graph the graph the subject to embed is in. * @param subjectStack the current stack of subjects. * * @return true if a circular reference would be created, false if not. */ function _createsCircularReference(subjectToEmbed, graph, subjectStack) { for(let i = subjectStack.length - 1; i >= 0; --i) { const subject = subjectStack[i]; if(subject.graph === graph && subject.subject['@id'] === subjectToEmbed['@id']) { return true; } } return false; } /** * Gets the frame flag value for the given flag name. * * @param frame the frame. * @param options the framing options. * @param name the flag name. * * @return the flag value. */ function _getFrameFlag(frame, options, name) { const flag = '@' + name; let rval = (flag in frame ? frame[flag][0] : options[name]); if(name === 'embed') { // default is "@last" // backwards-compatibility support for "embed" maps: // true => "@last" // false => "@never" if(rval === true) { rval = '@once'; } else if(rval === false) { rval = '@never'; } else if(rval !== '@always' && rval !== '@never' && rval !== '@link' && rval !== '@first' && rval !== '@last' && rval !== '@once') { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid value of @embed.', 'jsonld.SyntaxError', {code: 'invalid @embed value', frame}); } } return rval; } /** * Validates a JSON-LD frame, throwing an exception if the frame is invalid. * * @param frame the frame to validate. */ function _validateFrame(frame) { if(!types.isArray(frame) || frame.length !== 1 || !types.isObject(frame[0])) { throw new JsonLdError( 'Invalid JSON-LD syntax; a JSON-LD frame must be a single object.', 'jsonld.SyntaxError', {frame}); } if('@id' in frame[0]) { for(const id of util.asArray(frame[0]['@id'])) { // @id must be wildcard or an IRI if(!(types.isObject(id) || url.isAbsolute(id)) || (types.isString(id) && id.indexOf('_:') === 0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid @id in frame.', 'jsonld.SyntaxError', {code: 'invalid frame', frame}); } } } if('@type' in frame[0]) { for(const type of util.asArray(frame[0]['@type'])) { // @id must be wildcard or an IRI if(!(types.isObject(type) || url.isAbsolute(type)) || (types.isString(type) && type.indexOf('_:') === 0)) { throw new JsonLdError( 'Invalid JSON-LD syntax; invalid @type in frame.', 'jsonld.SyntaxError', {code: 'invalid frame', frame}); } } } } /** * Returns a map of all of the subjects that match a parsed frame. * * @param state the current framing state. * @param subjects the set of subjects to filter. * @param frame the parsed frame. * @param flags the frame flags. * * @return all of the matched subjects. */ function _filterSubjects(state, subjects, frame, flags) { // filter subjects in @id order const rval = {}; for(const id of subjects) { const subject = state.graphMap[state.graph][id]; if(_filterSubject(state, subject, frame, flags)) { rval[id] = subject; } } return rval; } /** * Returns true if the given subject matches the given frame. * * Matches either based on explicit type inclusion where the node has any * type listed in the frame. If the frame has empty types defined matches * nodes not having a @type. If the frame has a type of {} defined matches * nodes having any type defined. * * Otherwise, does duck typing, where the node must have all of the * properties defined in the frame. * * @param state the current framing state. * @param subject the subject to check. * @param frame the frame to check. * @param flags the frame flags. * * @return true if the subject matches, false if not. */ function _filterSubject(state, subject, frame, flags) { // check ducktype let wildcard = true; let matchesSome = false; for(const key in frame) { let matchThis = false; const nodeValues = util.getValues(subject, key); const isEmpty = util.getValues(frame, key).length === 0; if(key === '@id') { // match on no @id or any matching @id, including wildcard if(types.isEmptyObject(frame['@id'][0] || {})) { matchThis = true; } else if(frame['@id'].length >= 0) { matchThis = frame['@id'].includes(nodeValues[0]); } if(!flags.requireAll) { return matchThis; } } else if(key === '@type') { // check @type (object value means 'any' type, // fall through to ducktyping) wildcard = false; if(isEmpty) { if(nodeValues.length > 0) { // don't match on no @type return false; } matchThis = true; } else if(frame['@type'].length === 1 && types.isEmptyObject(frame['@type'][0])) { // match on wildcard @type if there is a type matchThis = nodeValues.length > 0; } else { // match on a specific @type for(const type of frame['@type']) { if(types.isObject(type) && '@default' in type) { // match on default object matchThis = true; } else { matchThis = matchThis || nodeValues.some(tt => tt === type); } } } if(!flags.requireAll) { return matchThis; } } else if(isKeyword(key)) { continue; } else { // Force a copy of this frame entry so it can be manipulated const thisFrame = util.getValues(frame, key)[0]; let hasDefault = false; if(thisFrame) { _validateFrame([thisFrame]); hasDefault = '@default' in thisFrame; } // no longer a wildcard pattern if frame has any non-keyword properties wildcard = false; // skip, but allow match if node has no value for property, and frame has // a default value if(nodeValues.length === 0 && hasDefault) { continue; } // if frame value is empty, don't match if subject has any value if(nodeValues.length > 0 && isEmpty) { return false; } if(thisFrame === undefined) { // node does not match if values is not empty and the value of property // in frame is match none. if(nodeValues.length > 0) { return false; } matchThis = true; } else { if(graphTypes.isList(thisFrame)) { const listValue = thisFrame['@list'][0]; if(graphTypes.isList(nodeValues[0])) { const nodeListValues = nodeValues[0]['@list']; if(graphTypes.isValue(listValue)) { // match on any matching value matchThis = nodeListValues.some(lv => _valueMatch(listValue, lv)); } else if(graphTypes.isSubject(listValue) || graphTypes.isSubjectReference(listValue)) { matchThis = nodeListValues.some(lv => _nodeMatch( state, listValue, lv, flags)); } } } else if(graphTypes.isValue(thisFrame)) { matchThis = nodeValues.some(nv => _valueMatch(thisFrame, nv)); } else if(graphTypes.isSubjectReference(thisFrame)) { matchThis = nodeValues.some(nv => _nodeMatch(state, thisFrame, nv, flags)); } else if(types.isObject(thisFrame)) { matchThis = nodeValues.length > 0; } else { matchThis = false; } } } // all non-defaulted values must match if requireAll is set if(!matchThis && flags.requireAll) { return false; } matchesSome = matchesSome || matchThis; } // return true if wildcard or subject matches some properties return wildcard || matchesSome; } /** * Removes an existing embed. * * @param state the current framing state. * @param id the @id of the embed to remove. */ function _removeEmbed(state, id) { // get existing embed const embeds = state.uniqueEmbeds[state.graph]; const embed = embeds[id]; const parent = embed.parent; const property = embed.property; // create reference to replace embed const subject = {'@id': id}; // remove existing embed if(types.isArray(parent)) { // replace subject with reference for(let i = 0; i < parent.length; ++i) { if(util.compareValues(parent[i], subject)) { parent[i] = subject; break; } } } else { // replace subject with reference const useArray = types.isArray(parent[property]); util.removeValue(parent, property, subject, {propertyIsArray: useArray}); util.addValue(parent, property, subject, {propertyIsArray: useArray}); } // recursively remove dependent dangling embeds const removeDependents = id => { // get embed keys as a separate array to enable deleting keys in map const ids = Object.keys(embeds); for(const next of ids) { if(next in embeds && types.isObject(embeds[next].parent) && embeds[next].parent['@id'] === id) { delete embeds[next]; removeDependents(next); } } }; removeDependents(id); } /** * Removes the @preserve keywords from expanded result of framing. * * @param input the framed, framed output. * @param options the framing options used. * * @return the resulting output. */ function _cleanupPreserve(input, options) { // recurse through arrays if(types.isArray(input)) { return input.map(value => _cleanupPreserve(value, options)); } if(types.isObject(input)) { // remove @preserve if('@preserve' in input) { return input['@preserve'][0]; } // skip @values if(graphTypes.isValue(input)) { return input; } // recurse through @lists if(graphTypes.isList(input)) { input['@list'] = _cleanupPreserve(input['@list'], options); return input; } // handle in-memory linked nodes if('@id' in input) { const id = input['@id']; if(options.link.hasOwnProperty(id)) { const idx = options.link[id].indexOf(input); if(idx !== -1) { // already visited return options.link[id][idx]; } // prevent circular visitation options.link[id].push(input); } else { // prevent circular visitation options.link[id] = [input]; } } // recurse through properties for(const prop in input) { // potentially remove the id, if it is an unreference bnode if(prop === '@id' && options.bnodesToClear.includes(input[prop])) { delete input['@id']; continue; } input[prop] = _cleanupPreserve(input[prop], options); } } return input; } /** * Adds framing output to the given parent. * * @param parent the parent to add to. * @param property the parent property. * @param output the output to add. */ function _addFrameOutput(parent, property, output) { if(types.isObject(parent)) { util.addValue(parent, property, output, {propertyIsArray: true}); } else { parent.push(output); } } /** * Node matches if it is a node, and matches the pattern as a frame. * * @param state the current framing state. * @param pattern used to match value * @param value to check * @param flags the frame flags. */ function _nodeMatch(state, pattern, value, flags) { if(!('@id' in value)) { return false; } const nodeObject = state.subjects[value['@id']]; return nodeObject && _filterSubject(state, nodeObject, pattern, flags); } /** * Value matches if it is a value and matches the value pattern * * * `pattern` is empty * * @values are the same, or `pattern[@value]` is a wildcard, and * * @types are the same or `value[@type]` is not null * and `pattern[@type]` is `{}`, or `value[@type]` is null * and `pattern[@type]` is null or `[]`, and * * @languages are the same or `value[@language]` is not null * and `pattern[@language]` is `{}`, or `value[@language]` is null * and `pattern[@language]` is null or `[]`. * * @param pattern used to match value * @param value to check */ function _valueMatch(pattern, value) { const v1 = value['@value']; const t1 = value['@type']; const l1 = value['@language']; const v2 = pattern['@value'] ? (types.isArray(pattern['@value']) ? pattern['@value'] : [pattern['@value']]) : []; const t2 = pattern['@type'] ? (types.isArray(pattern['@type']) ? pattern['@type'] : [pattern['@type']]) : []; const l2 = pattern['@language'] ? (types.isArray(pattern['@language']) ? pattern['@language'] : [pattern['@language']]) : []; if(v2.length === 0 && t2.length === 0 && l2.length === 0) { return true; } if(!(v2.includes(v1) || types.isEmptyObject(v2[0]))) { return false; } if(!(!t1 && t2.length === 0 || t2.includes(t1) || t1 && types.isEmptyObject(t2[0]))) { return false; } if(!(!l1 && l2.length === 0 || l2.includes(l1) || l1 && types.isEmptyObject(l2[0]))) { return false; } return true; } jsonld.js-4.0.1/lib/fromRdf.js000066400000000000000000000226441401135163200161270ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const JsonLdError = require('./JsonLdError'); const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); // constants const { // RDF, RDF_LIST, RDF_FIRST, RDF_REST, RDF_NIL, RDF_TYPE, // RDF_PLAIN_LITERAL, // RDF_XML_LITERAL, RDF_JSON_LITERAL, // RDF_OBJECT, // RDF_LANGSTRING, // XSD, XSD_BOOLEAN, XSD_DOUBLE, XSD_INTEGER, XSD_STRING, } = require('./constants'); const REGEX_BCP47 = /^[a-zA-Z]{1,8}(-[a-zA-Z0-9]{1,8})*$/; const api = {}; module.exports = api; /** * Converts an RDF dataset to JSON-LD. * * @param dataset the RDF dataset. * @param options the RDF serialization options. * * @return a Promise that resolves to the JSON-LD output. */ api.fromRDF = async ( dataset, { useRdfType = false, useNativeTypes = false, rdfDirection = null } ) => { const defaultGraph = {}; const graphMap = {'@default': defaultGraph}; const referencedOnce = {}; for(const quad of dataset) { // TODO: change 'name' to 'graph' const name = (quad.graph.termType === 'DefaultGraph') ? '@default' : quad.graph.value; if(!(name in graphMap)) { graphMap[name] = {}; } if(name !== '@default' && !(name in defaultGraph)) { defaultGraph[name] = {'@id': name}; } const nodeMap = graphMap[name]; // get subject, predicate, object const s = quad.subject.value; const p = quad.predicate.value; const o = quad.object; if(!(s in nodeMap)) { nodeMap[s] = {'@id': s}; } const node = nodeMap[s]; const objectIsNode = o.termType.endsWith('Node'); if(objectIsNode && !(o.value in nodeMap)) { nodeMap[o.value] = {'@id': o.value}; } if(p === RDF_TYPE && !useRdfType && objectIsNode) { util.addValue(node, '@type', o.value, {propertyIsArray: true}); continue; } const value = _RDFToObject(o, useNativeTypes, rdfDirection); util.addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily // until all triples are read if(objectIsNode) { if(o.value === RDF_NIL) { // track rdf:nil uniquely per graph const object = nodeMap[o.value]; if(!('usages' in object)) { object.usages = []; } object.usages.push({ node, property: p, value }); } else if(o.value in referencedOnce) { // object referenced more than once referencedOnce[o.value] = false; } else { // keep track of single reference referencedOnce[o.value] = { node, property: p, value }; } } } /* for(let name in dataset) { const graph = dataset[name]; if(!(name in graphMap)) { graphMap[name] = {}; } if(name !== '@default' && !(name in defaultGraph)) { defaultGraph[name] = {'@id': name}; } const nodeMap = graphMap[name]; for(let ti = 0; ti < graph.length; ++ti) { const triple = graph[ti]; // get subject, predicate, object const s = triple.subject.value; const p = triple.predicate.value; const o = triple.object; if(!(s in nodeMap)) { nodeMap[s] = {'@id': s}; } const node = nodeMap[s]; const objectIsId = (o.type === 'IRI' || o.type === 'blank node'); if(objectIsId && !(o.value in nodeMap)) { nodeMap[o.value] = {'@id': o.value}; } if(p === RDF_TYPE && !useRdfType && objectIsId) { util.addValue(node, '@type', o.value, {propertyIsArray: true}); continue; } const value = _RDFToObject(o, useNativeTypes); util.addValue(node, p, value, {propertyIsArray: true}); // object may be an RDF list/partial list node but we can't know easily // until all triples are read if(objectIsId) { if(o.value === RDF_NIL) { // track rdf:nil uniquely per graph const object = nodeMap[o.value]; if(!('usages' in object)) { object.usages = []; } object.usages.push({ node: node, property: p, value: value }); } else if(o.value in referencedOnce) { // object referenced more than once referencedOnce[o.value] = false; } else { // keep track of single reference referencedOnce[o.value] = { node: node, property: p, value: value }; } } } }*/ // convert linked lists to @list arrays for(const name in graphMap) { const graphObject = graphMap[name]; // no @lists to be converted, continue if(!(RDF_NIL in graphObject)) { continue; } // iterate backwards through each RDF list const nil = graphObject[RDF_NIL]; if(!nil.usages) { continue; } for(let usage of nil.usages) { let node = usage.node; let property = usage.property; let head = usage.value; const list = []; const listNodes = []; // ensure node is a well-formed list node; it must: // 1. Be referenced only once. // 2. Have an array for rdf:first that has 1 item. // 3. Have an array for rdf:rest that has 1 item. // 4. Have no keys other than: @id, rdf:first, rdf:rest, and, // optionally, @type where the value is rdf:List. let nodeKeyCount = Object.keys(node).length; while(property === RDF_REST && types.isObject(referencedOnce[node['@id']]) && types.isArray(node[RDF_FIRST]) && node[RDF_FIRST].length === 1 && types.isArray(node[RDF_REST]) && node[RDF_REST].length === 1 && (nodeKeyCount === 3 || (nodeKeyCount === 4 && types.isArray(node['@type']) && node['@type'].length === 1 && node['@type'][0] === RDF_LIST))) { list.push(node[RDF_FIRST][0]); listNodes.push(node['@id']); // get next node, moving backwards through list usage = referencedOnce[node['@id']]; node = usage.node; property = usage.property; head = usage.value; nodeKeyCount = Object.keys(node).length; // if node is not a blank node, then list head found if(!graphTypes.isBlankNode(node)) { break; } } // transform list into @list object delete head['@id']; head['@list'] = list.reverse(); for(const listNode of listNodes) { delete graphObject[listNode]; } } delete nil.usages; } const result = []; const subjects = Object.keys(defaultGraph).sort(); for(const subject of subjects) { const node = defaultGraph[subject]; if(subject in graphMap) { const graph = node['@graph'] = []; const graphObject = graphMap[subject]; const graphSubjects = Object.keys(graphObject).sort(); for(const graphSubject of graphSubjects) { const node = graphObject[graphSubject]; // only add full subjects to top-level if(!graphTypes.isSubjectReference(node)) { graph.push(node); } } } // only add full subjects to top-level if(!graphTypes.isSubjectReference(node)) { result.push(node); } } return result; }; /** * Converts an RDF triple object to a JSON-LD object. * * @param o the RDF triple object to convert. * @param useNativeTypes true to output native types, false not to. * * @return the JSON-LD object. */ function _RDFToObject(o, useNativeTypes, rdfDirection) { // convert NamedNode/BlankNode object to JSON-LD if(o.termType.endsWith('Node')) { return {'@id': o.value}; } // convert literal to JSON-LD const rval = {'@value': o.value}; // add language if(o.language) { rval['@language'] = o.language; } else { let type = o.datatype.value; if(!type) { type = XSD_STRING; } if(type === RDF_JSON_LITERAL) { type = '@json'; try { rval['@value'] = JSON.parse(rval['@value']); } catch(e) { throw new JsonLdError( 'JSON literal could not be parsed.', 'jsonld.InvalidJsonLiteral', {code: 'invalid JSON literal', value: rval['@value'], cause: e}); } } // use native types for certain xsd types if(useNativeTypes) { if(type === XSD_BOOLEAN) { if(rval['@value'] === 'true') { rval['@value'] = true; } else if(rval['@value'] === 'false') { rval['@value'] = false; } } else if(types.isNumeric(rval['@value'])) { if(type === XSD_INTEGER) { const i = parseInt(rval['@value'], 10); if(i.toFixed(0) === rval['@value']) { rval['@value'] = i; } } else if(type === XSD_DOUBLE) { rval['@value'] = parseFloat(rval['@value']); } } // do not add native type if(![XSD_BOOLEAN, XSD_INTEGER, XSD_DOUBLE, XSD_STRING].includes(type)) { rval['@type'] = type; } } else if(rdfDirection === 'i18n-datatype' && type.startsWith('https://www.w3.org/ns/i18n#')) { const [, language, direction] = type.split(/[#_]/); if(language.length > 0) { rval['@language'] = language; if(!language.match(REGEX_BCP47)) { console.warn(`@language must be valid BCP47: ${language}`); } } rval['@direction'] = direction; } else if(type !== XSD_STRING) { rval['@type'] = type; } } return rval; } jsonld.js-4.0.1/lib/graphTypes.js000066400000000000000000000062541401135163200166550ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const types = require('./types'); const api = {}; module.exports = api; /** * Returns true if the given value is a subject with properties. * * @param v the value to check. * * @return true if the value is a subject with properties, false if not. */ api.isSubject = v => { // Note: A value is a subject if all of these hold true: // 1. It is an Object. // 2. It is not a @value, @set, or @list. // 3. It has more than 1 key OR any existing key is not @id. if(types.isObject(v) && !(('@value' in v) || ('@set' in v) || ('@list' in v))) { const keyCount = Object.keys(v).length; return (keyCount > 1 || !('@id' in v)); } return false; }; /** * Returns true if the given value is a subject reference. * * @param v the value to check. * * @return true if the value is a subject reference, false if not. */ api.isSubjectReference = v => // Note: A value is a subject reference if all of these hold true: // 1. It is an Object. // 2. It has a single key: @id. (types.isObject(v) && Object.keys(v).length === 1 && ('@id' in v)); /** * Returns true if the given value is a @value. * * @param v the value to check. * * @return true if the value is a @value, false if not. */ api.isValue = v => // Note: A value is a @value if all of these hold true: // 1. It is an Object. // 2. It has the @value property. types.isObject(v) && ('@value' in v); /** * Returns true if the given value is a @list. * * @param v the value to check. * * @return true if the value is a @list, false if not. */ api.isList = v => // Note: A value is a @list if all of these hold true: // 1. It is an Object. // 2. It has the @list property. types.isObject(v) && ('@list' in v); /** * Returns true if the given value is a @graph. * * @return true if the value is a @graph, false if not. */ api.isGraph = v => { // Note: A value is a graph if all of these hold true: // 1. It is an object. // 2. It has an `@graph` key. // 3. It may have '@id' or '@index' return types.isObject(v) && '@graph' in v && Object.keys(v) .filter(key => key !== '@id' && key !== '@index').length === 1; }; /** * Returns true if the given value is a simple @graph. * * @return true if the value is a simple @graph, false if not. */ api.isSimpleGraph = v => { // Note: A value is a simple graph if all of these hold true: // 1. It is an object. // 2. It has an `@graph` key. // 3. It has only 1 key or 2 keys where one of them is `@index`. return api.isGraph(v) && !('@id' in v); }; /** * Returns true if the given value is a blank node. * * @param v the value to check. * * @return true if the value is a blank node, false if not. */ api.isBlankNode = v => { // Note: A value is a blank node if all of these hold true: // 1. It is an Object. // 2. If it has an @id key its value begins with '_:'. // 3. It has no keys OR is not a @value, @set, or @list. if(types.isObject(v)) { if('@id' in v) { return (v['@id'].indexOf('_:') === 0); } return (Object.keys(v).length === 0 || !(('@value' in v) || ('@set' in v) || ('@list' in v))); } return false; }; jsonld.js-4.0.1/lib/index.js000066400000000000000000000007351401135163200156340ustar00rootroot00000000000000/** * jsonld.js library. * * @author Dave Longley * * Copyright 2010-2017 Digital Bazaar, Inc. */ // FIXME: remove after change to core-js 3 and dropping node6 support const fromEntries = require('object.fromentries'); if(!Object.fromEntries) { fromEntries.shim(); } if(require('semver').gte(process.version, '8.6.0')) { module.exports = require('./jsonld'); } else { require('core-js/fn/object/entries'); module.exports = require('../dist/node6/lib/jsonld'); } jsonld.js-4.0.1/lib/jsonld.js000066400000000000000000000773401401135163200160240ustar00rootroot00000000000000/** * A JavaScript implementation of the JSON-LD API. * * @author Dave Longley * * @license BSD 3-Clause License * Copyright (c) 2011-2019 Digital Bazaar, Inc. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * Neither the name of the Digital Bazaar, Inc. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ const canonize = require('rdf-canonize'); const util = require('./util'); const ContextResolver = require('./ContextResolver'); const IdentifierIssuer = util.IdentifierIssuer; const JsonLdError = require('./JsonLdError'); const LRU = require('lru-cache'); const NQuads = require('./NQuads'); const {expand: _expand} = require('./expand'); const {flatten: _flatten} = require('./flatten'); const {fromRDF: _fromRDF} = require('./fromRdf'); const {toRDF: _toRDF} = require('./toRdf'); const { frameMergedOrDefault: _frameMergedOrDefault, cleanupNull: _cleanupNull } = require('./frame'); const { isArray: _isArray, isObject: _isObject, isString: _isString } = require('./types'); const { isSubjectReference: _isSubjectReference, } = require('./graphTypes'); const { expandIri: _expandIri, getInitialContext: _getInitialContext, process: _processContext, processingMode: _processingMode } = require('./context'); const { compact: _compact, compactIri: _compactIri } = require('./compact'); const { createNodeMap: _createNodeMap, createMergedNodeMap: _createMergedNodeMap, mergeNodeMaps: _mergeNodeMaps } = require('./nodeMap'); // determine if in-browser or using Node.js const _nodejs = ( typeof process !== 'undefined' && process.versions && process.versions.node); const _browser = !_nodejs && (typeof window !== 'undefined' || typeof self !== 'undefined'); /* eslint-disable indent */ // attaches jsonld API to the given object const wrapper = function(jsonld) { /** Registered RDF dataset parsers hashed by content-type. */ const _rdfParsers = {}; // resolved context cache // TODO: consider basing max on context size rather than number const RESOLVED_CONTEXT_CACHE_MAX_SIZE = 100; const _resolvedContextCache = new LRU({max: RESOLVED_CONTEXT_CACHE_MAX_SIZE}); /* Core API */ /** * Performs JSON-LD compaction. * * @param input the JSON-LD input to compact. * @param ctx the context to compact with. * @param [options] options to use: * [base] the base IRI to use. * [compactArrays] true to compact arrays to single values when * appropriate, false not to (default: true). * [compactToRelative] true to compact IRIs to be relative to document * base, false to keep absolute (default: true) * [graph] true to always output a top-level graph (default: false). * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. * [documentLoader(url, options)] the document loader. * [expansionMap(info)] a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. * [framing] true if compaction is occuring during a framing operation. * [compactionMap(info)] a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. * [contextResolver] internal use only. * * @return a Promise that resolves to the compacted output. */ jsonld.compact = async function(input, ctx, options) { if(arguments.length < 2) { throw new TypeError('Could not compact, too few arguments.'); } if(ctx === null) { throw new JsonLdError( 'The compaction context must not be null.', 'jsonld.CompactError', {code: 'invalid local context'}); } // nothing to compact if(input === null) { return null; } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', compactArrays: true, compactToRelative: true, graph: false, skipExpansion: false, link: false, issuer: new IdentifierIssuer('_:b'), contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); if(options.link) { // force skip expansion when linking, "link" is not part of the public // API, it should only be called from framing options.skipExpansion = true; } if(!options.compactToRelative) { delete options.base; } // expand input let expanded; if(options.skipExpansion) { expanded = input; } else { expanded = await jsonld.expand(input, options); } // process context const activeCtx = await jsonld.processContext( _getInitialContext(options), ctx, options); // do compaction let compacted = await _compact({ activeCtx, element: expanded, options, compactionMap: options.compactionMap }); // perform clean up if(options.compactArrays && !options.graph && _isArray(compacted)) { if(compacted.length === 1) { // simplify to a single item compacted = compacted[0]; } else if(compacted.length === 0) { // simplify to an empty object compacted = {}; } } else if(options.graph && _isObject(compacted)) { // always use array if graph option is on compacted = [compacted]; } // follow @context key if(_isObject(ctx) && '@context' in ctx) { ctx = ctx['@context']; } // build output context ctx = util.clone(ctx); if(!_isArray(ctx)) { ctx = [ctx]; } // remove empty contexts const tmp = ctx; ctx = []; for(let i = 0; i < tmp.length; ++i) { if(!_isObject(tmp[i]) || Object.keys(tmp[i]).length > 0) { ctx.push(tmp[i]); } } // remove array if only one context const hasContext = (ctx.length > 0); if(ctx.length === 1) { ctx = ctx[0]; } // add context and/or @graph if(_isArray(compacted)) { // use '@graph' keyword const graphAlias = _compactIri({ activeCtx, iri: '@graph', relativeTo: {vocab: true} }); const graph = compacted; compacted = {}; if(hasContext) { compacted['@context'] = ctx; } compacted[graphAlias] = graph; } else if(_isObject(compacted) && hasContext) { // reorder keys so @context is first const graph = compacted; compacted = {'@context': ctx}; for(const key in graph) { compacted[key] = graph[key]; } } return compacted; }; /** * Performs JSON-LD expansion. * * @param input the JSON-LD input to expand. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [keepFreeFloatingNodes] true to keep free-floating nodes, * false not to, defaults to false. * [documentLoader(url, options)] the document loader. * [expansionMap(info)] a function that can be used to custom map * unmappable values (or to throw an error when they are detected); * if this function returns `undefined` then the default behavior * will be used. * [contextResolver] internal use only. * * @return a Promise that resolves to the expanded output. */ jsonld.expand = async function(input, options) { if(arguments.length < 1) { throw new TypeError('Could not expand, too few arguments.'); } // set default options options = _setDefaults(options, { keepFreeFloatingNodes: false, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); if(options.expansionMap === false) { options.expansionMap = undefined; } // build set of objects that may have @contexts to resolve const toResolve = {}; // build set of contexts to process prior to expansion const contextsToProcess = []; // if an `expandContext` has been given ensure it gets resolved if('expandContext' in options) { const expandContext = util.clone(options.expandContext); if(_isObject(expandContext) && '@context' in expandContext) { toResolve.expandContext = expandContext; } else { toResolve.expandContext = {'@context': expandContext}; } contextsToProcess.push(toResolve.expandContext); } // if input is a string, attempt to dereference remote document let defaultBase; if(!_isString(input)) { // input is not a URL, do not need to retrieve it first toResolve.input = util.clone(input); } else { // load remote doc const remoteDoc = await jsonld.get(input, options); defaultBase = remoteDoc.documentUrl; toResolve.input = remoteDoc.document; if(remoteDoc.contextUrl) { // context included in HTTP link header and must be resolved toResolve.remoteContext = {'@context': remoteDoc.contextUrl}; contextsToProcess.push(toResolve.remoteContext); } } // set default base if(!('base' in options)) { options.base = defaultBase || ''; } // process any additional contexts let activeCtx = _getInitialContext(options); for(const localCtx of contextsToProcess) { activeCtx = await _processContext({activeCtx, localCtx, options}); } // expand resolved input let expanded = await _expand({ activeCtx, element: toResolve.input, options, expansionMap: options.expansionMap }); // optimize away @graph with no other properties if(_isObject(expanded) && ('@graph' in expanded) && Object.keys(expanded).length === 1) { expanded = expanded['@graph']; } else if(expanded === null) { expanded = []; } // normalize to an array if(!_isArray(expanded)) { expanded = [expanded]; } return expanded; }; /** * Performs JSON-LD flattening. * * @param input the JSON-LD to flatten. * @param ctx the context to use to compact the flattened output, or null. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the flattened output. */ jsonld.flatten = async function(input, ctx, options) { if(arguments.length < 1) { return new TypeError('Could not flatten, too few arguments.'); } if(typeof ctx === 'function') { ctx = null; } else { ctx = ctx || null; } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // expand input const expanded = await jsonld.expand(input, options); // do flattening const flattened = _flatten(expanded); if(ctx === null) { // no compaction required return flattened; } // compact result (force @graph option to true, skip expansion) options.graph = true; options.skipExpansion = true; const compacted = await jsonld.compact(flattened, ctx, options); return compacted; }; /** * Performs JSON-LD framing. * * @param input the JSON-LD input to frame. * @param frame the JSON-LD frame to use. * @param [options] the framing options. * [base] the base IRI to use. * [expandContext] a context to expand with. * [embed] default @embed flag: '@last', '@always', '@never', '@link' * (default: '@last'). * [explicit] default @explicit flag (default: false). * [requireAll] default @requireAll flag (default: true). * [omitDefault] default @omitDefault flag (default: false). * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the framed output. */ jsonld.frame = async function(input, frame, options) { if(arguments.length < 2) { throw new TypeError('Could not frame, too few arguments.'); } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', embed: '@once', explicit: false, requireAll: false, omitDefault: false, bnodesToClear: [], contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // if frame is a string, attempt to dereference remote document if(_isString(frame)) { // load remote doc const remoteDoc = await jsonld.get(frame, options); frame = remoteDoc.document; if(remoteDoc.contextUrl) { // inject link header @context into frame let ctx = frame['@context']; if(!ctx) { ctx = remoteDoc.contextUrl; } else if(_isArray(ctx)) { ctx.push(remoteDoc.contextUrl); } else { ctx = [ctx, remoteDoc.contextUrl]; } frame['@context'] = ctx; } } const frameContext = frame ? frame['@context'] || {} : {}; // process context const activeCtx = await jsonld.processContext( _getInitialContext(options), frameContext, options); // mode specific defaults if(!options.hasOwnProperty('omitGraph')) { options.omitGraph = _processingMode(activeCtx, 1.1); } if(!options.hasOwnProperty('pruneBlankNodeIdentifiers')) { options.pruneBlankNodeIdentifiers = _processingMode(activeCtx, 1.1); } // expand input const expanded = await jsonld.expand(input, options); // expand frame const opts = {...options}; opts.isFrame = true; opts.keepFreeFloatingNodes = true; const expandedFrame = await jsonld.expand(frame, opts); // if the unexpanded frame includes a key expanding to @graph, frame the // default graph, otherwise, the merged graph const frameKeys = Object.keys(frame) .map(key => _expandIri(activeCtx, key, {vocab: true})); opts.merged = !frameKeys.includes('@graph'); opts.is11 = _processingMode(activeCtx, 1.1); // do framing const framed = _frameMergedOrDefault(expanded, expandedFrame, opts); opts.graph = !options.omitGraph; opts.skipExpansion = true; opts.link = {}; opts.framing = true; let compacted = await jsonld.compact(framed, frameContext, opts); // replace @null with null, compacting arrays opts.link = {}; compacted = _cleanupNull(compacted, opts); return compacted; }; /** * **Experimental** * * Links a JSON-LD document's nodes in memory. * * @param input the JSON-LD document to link. * @param [ctx] the JSON-LD context to apply. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the linked output. */ jsonld.link = async function(input, ctx, options) { // API matches running frame with a wildcard frame and embed: '@link' // get arguments const frame = {}; if(ctx) { frame['@context'] = ctx; } frame['@embed'] = '@link'; return jsonld.frame(input, frame, options); }; /** * Performs RDF dataset normalization on the given input. The input is JSON-LD * unless the 'inputFormat' option is used. The output is an RDF dataset * unless the 'format' option is used. * * @param input the input to normalize as JSON-LD or as a format specified by * the 'inputFormat' option. * @param [options] the options to use: * [algorithm] the normalization algorithm to use, `URDNA2015` or * `URGNA2012` (default: `URDNA2015`). * [base] the base IRI to use. * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. * [inputFormat] the format if input is not JSON-LD: * 'application/n-quads' for N-Quads. * [format] the format if output is a string: * 'application/n-quads' for N-Quads. * [documentLoader(url, options)] the document loader. * [useNative] true to use a native canonize algorithm * [contextResolver] internal use only. * * @return a Promise that resolves to the normalized output. */ jsonld.normalize = jsonld.canonize = async function(input, options) { if(arguments.length < 1) { throw new TypeError('Could not canonize, too few arguments.'); } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', algorithm: 'URDNA2015', skipExpansion: false, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); if('inputFormat' in options) { if(options.inputFormat !== 'application/n-quads' && options.inputFormat !== 'application/nquads') { throw new JsonLdError( 'Unknown canonicalization input format.', 'jsonld.CanonizeError'); } // TODO: `await` for async parsers const parsedInput = NQuads.parse(input); // do canonicalization return canonize.canonize(parsedInput, options); } // convert to RDF dataset then do normalization const opts = {...options}; delete opts.format; opts.produceGeneralizedRdf = false; const dataset = await jsonld.toRDF(input, opts); // do canonicalization return canonize.canonize(dataset, options); }; /** * Converts an RDF dataset to JSON-LD. * * @param dataset a serialized string of RDF in a format specified by the * format option or an RDF dataset to convert. * @param [options] the options to use: * [format] the format if dataset param must first be parsed: * 'application/n-quads' for N-Quads (default). * [rdfParser] a custom RDF-parser to use to parse the dataset. * [useRdfType] true to use rdf:type, false to use @type * (default: false). * [useNativeTypes] true to convert XSD types into native types * (boolean, integer, double), false not to (default: false). * * @return a Promise that resolves to the JSON-LD document. */ jsonld.fromRDF = async function(dataset, options) { if(arguments.length < 1) { throw new TypeError('Could not convert from RDF, too few arguments.'); } // set default options options = _setDefaults(options, { format: _isString(dataset) ? 'application/n-quads' : undefined }); const {format} = options; let {rdfParser} = options; // handle special format if(format) { // check supported formats rdfParser = rdfParser || _rdfParsers[format]; if(!rdfParser) { throw new JsonLdError( 'Unknown input format.', 'jsonld.UnknownFormat', {format}); } } else { // no-op parser, assume dataset already parsed rdfParser = () => dataset; } // rdfParser must be synchronous or return a promise, no callback support const parsedDataset = await rdfParser(dataset); return _fromRDF(parsedDataset, options); }; /** * Outputs the RDF dataset found in the given JSON-LD object. * * @param input the JSON-LD input. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [skipExpansion] true to assume the input is expanded and skip * expansion, false not to, defaults to false. * [format] the format to use to output a string: * 'application/n-quads' for N-Quads. * [produceGeneralizedRdf] true to output generalized RDF, false * to produce only standard RDF (default: false). * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the RDF dataset. */ jsonld.toRDF = async function(input, options) { if(arguments.length < 1) { throw new TypeError('Could not convert to RDF, too few arguments.'); } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', skipExpansion: false, contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // TODO: support toRDF custom map? let expanded; if(options.skipExpansion) { expanded = input; } else { // expand input expanded = await jsonld.expand(input, options); } // output RDF dataset const dataset = _toRDF(expanded, options); if(options.format) { if(options.format === 'application/n-quads' || options.format === 'application/nquads') { return await NQuads.serialize(dataset); } throw new JsonLdError( 'Unknown output format.', 'jsonld.UnknownFormat', {format: options.format}); } return dataset; }; /** * **Experimental** * * Recursively flattens the nodes in the given JSON-LD input into a merged * map of node ID => node. All graphs will be merged into the default graph. * * @param input the JSON-LD input. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged node map. */ jsonld.createNodeMap = async function(input, options) { if(arguments.length < 1) { throw new TypeError('Could not create node map, too few arguments.'); } // set default options options = _setDefaults(options, { base: _isString(input) ? input : '', contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // expand input const expanded = await jsonld.expand(input, options); return _createMergedNodeMap(expanded, options); }; /** * **Experimental** * * Merges two or more JSON-LD documents into a single flattened document. * * @param docs the JSON-LD documents to merge together. * @param ctx the context to use to compact the merged result, or null. * @param [options] the options to use: * [base] the base IRI to use. * [expandContext] a context to expand with. * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * [mergeNodes] true to merge properties for nodes with the same ID, * false to ignore new properties for nodes with the same ID once * the ID has been defined; note that this may not prevent merging * new properties where a node is in the `object` position * (default: true). * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the merged output. */ jsonld.merge = async function(docs, ctx, options) { if(arguments.length < 1) { throw new TypeError('Could not merge, too few arguments.'); } if(!_isArray(docs)) { throw new TypeError('Could not merge, "docs" must be an array.'); } if(typeof ctx === 'function') { ctx = null; } else { ctx = ctx || null; } // set default options options = _setDefaults(options, { contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // expand all documents const expanded = await Promise.all(docs.map(doc => { const opts = {...options}; return jsonld.expand(doc, opts); })); let mergeNodes = true; if('mergeNodes' in options) { mergeNodes = options.mergeNodes; } const issuer = options.issuer || new IdentifierIssuer('_:b'); const graphs = {'@default': {}}; for(let i = 0; i < expanded.length; ++i) { // uniquely relabel blank nodes const doc = util.relabelBlankNodes(expanded[i], { issuer: new IdentifierIssuer('_:b' + i + '-') }); // add nodes to the shared node map graphs if merging nodes, to a // separate graph set if not const _graphs = (mergeNodes || i === 0) ? graphs : {'@default': {}}; _createNodeMap(doc, _graphs, '@default', issuer); if(_graphs !== graphs) { // merge document graphs but don't merge existing nodes for(const graphName in _graphs) { const _nodeMap = _graphs[graphName]; if(!(graphName in graphs)) { graphs[graphName] = _nodeMap; continue; } const nodeMap = graphs[graphName]; for(const key in _nodeMap) { if(!(key in nodeMap)) { nodeMap[key] = _nodeMap[key]; } } } } } // add all non-default graphs to default graph const defaultGraph = _mergeNodeMaps(graphs); // produce flattened output const flattened = []; const keys = Object.keys(defaultGraph).sort(); for(let ki = 0; ki < keys.length; ++ki) { const node = defaultGraph[keys[ki]]; // only add full subjects to top-level if(!_isSubjectReference(node)) { flattened.push(node); } } if(ctx === null) { return flattened; } // compact result (force @graph option to true, skip expansion) options.graph = true; options.skipExpansion = true; const compacted = await jsonld.compact(flattened, ctx, options); return compacted; }; /** * The default document loader for external documents. * * @param url the URL to load. * * @return a promise that resolves to the remote document. */ Object.defineProperty(jsonld, 'documentLoader', { get: () => jsonld._documentLoader, set: v => jsonld._documentLoader = v }); // default document loader not implemented jsonld.documentLoader = async url => { throw new JsonLdError( 'Could not retrieve a JSON-LD document from the URL. URL ' + 'dereferencing not implemented.', 'jsonld.LoadDocumentError', {code: 'loading document failed', url}); }; /** * Gets a remote JSON-LD document using the default document loader or * one given in the passed options. * * @param url the URL to fetch. * @param [options] the options to use: * [documentLoader] the document loader to use. * * @return a Promise that resolves to the retrieved remote document. */ jsonld.get = async function(url, options) { let load; if(typeof options.documentLoader === 'function') { load = options.documentLoader; } else { load = jsonld.documentLoader; } const remoteDoc = await load(url); try { if(!remoteDoc.document) { throw new JsonLdError( 'No remote document found at the given URL.', 'jsonld.NullRemoteDocument'); } if(_isString(remoteDoc.document)) { remoteDoc.document = JSON.parse(remoteDoc.document); } } catch(e) { throw new JsonLdError( 'Could not retrieve a JSON-LD document from the URL.', 'jsonld.LoadDocumentError', { code: 'loading document failed', cause: e, remoteDoc }); } return remoteDoc; }; /** * Processes a local context, resolving any URLs as necessary, and returns a * new active context. * * @param activeCtx the current active context. * @param localCtx the local context to process. * @param [options] the options to use: * [documentLoader(url, options)] the document loader. * [contextResolver] internal use only. * * @return a Promise that resolves to the new active context. */ jsonld.processContext = async function( activeCtx, localCtx, options) { // set default options options = _setDefaults(options, { base: '', contextResolver: new ContextResolver( {sharedCache: _resolvedContextCache}) }); // return initial context early for null context if(localCtx === null) { return _getInitialContext(options); } // get URLs in localCtx localCtx = util.clone(localCtx); if(!(_isObject(localCtx) && '@context' in localCtx)) { localCtx = {'@context': localCtx}; } return _processContext({activeCtx, localCtx, options}); }; // backwards compatibility jsonld.getContextValue = require('./context').getContextValue; /** * Document loaders. */ jsonld.documentLoaders = {}; jsonld.documentLoaders.node = require('./documentLoaders/node'); jsonld.documentLoaders.xhr = require('./documentLoaders/xhr'); /** * Assigns the default document loader for external document URLs to a built-in * default. Supported types currently include: 'xhr' and 'node'. * * @param type the type to set. * @param [params] the parameters required to use the document loader. */ jsonld.useDocumentLoader = function(type) { if(!(type in jsonld.documentLoaders)) { throw new JsonLdError( 'Unknown document loader type: "' + type + '"', 'jsonld.UnknownDocumentLoader', {type}); } // set document loader jsonld.documentLoader = jsonld.documentLoaders[type].apply( jsonld, Array.prototype.slice.call(arguments, 1)); }; /** * Registers an RDF dataset parser by content-type, for use with * jsonld.fromRDF. An RDF dataset parser will always be given one parameter, * a string of input. An RDF dataset parser can be synchronous or * asynchronous (by returning a promise). * * @param contentType the content-type for the parser. * @param parser(input) the parser function (takes a string as a parameter * and either returns an RDF dataset or a Promise that resolves to one. */ jsonld.registerRDFParser = function(contentType, parser) { _rdfParsers[contentType] = parser; }; /** * Unregisters an RDF dataset parser by content-type. * * @param contentType the content-type for the parser. */ jsonld.unregisterRDFParser = function(contentType) { delete _rdfParsers[contentType]; }; // register the N-Quads RDF parser jsonld.registerRDFParser('application/n-quads', NQuads.parse); jsonld.registerRDFParser('application/nquads', NQuads.parse); /* URL API */ jsonld.url = require('./url'); /* Utility API */ jsonld.util = util; // backwards compatibility Object.assign(jsonld, util); // reexpose API as jsonld.promises for backwards compatability jsonld.promises = jsonld; // backwards compatibility jsonld.RequestQueue = require('./RequestQueue'); /* WebIDL API */ jsonld.JsonLdProcessor = require('./JsonLdProcessor')(jsonld); // setup browser global JsonLdProcessor if(_browser && typeof global.JsonLdProcessor === 'undefined') { Object.defineProperty(global, 'JsonLdProcessor', { writable: true, enumerable: false, configurable: true, value: jsonld.JsonLdProcessor }); } // set platform-specific defaults/APIs if(_nodejs) { // use node document loader by default jsonld.useDocumentLoader('node'); } else if(typeof XMLHttpRequest !== 'undefined') { // use xhr document loader by default jsonld.useDocumentLoader('xhr'); } function _setDefaults(options, { documentLoader = jsonld.documentLoader, ...defaults }) { return Object.assign({}, {documentLoader}, defaults, options); } // end of jsonld API `wrapper` factory return jsonld; }; // external APIs: // used to generate a new jsonld API instance const factory = function() { return wrapper(function() { return factory(); }); }; // wrap the main jsonld API instance wrapper(factory); // export API module.exports = factory; jsonld.js-4.0.1/lib/nodeMap.js000066400000000000000000000202731401135163200161070ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); const types = require('./types'); const util = require('./util'); const JsonLdError = require('./JsonLdError'); const api = {}; module.exports = api; /** * Creates a merged JSON-LD node map (node ID => node). * * @param input the expanded JSON-LD to create a node map of. * @param [options] the options to use: * [issuer] a jsonld.IdentifierIssuer to use to label blank nodes. * * @return the node map. */ api.createMergedNodeMap = (input, options) => { options = options || {}; // produce a map of all subjects and name each bnode const issuer = options.issuer || new util.IdentifierIssuer('_:b'); const graphs = {'@default': {}}; api.createNodeMap(input, graphs, '@default', issuer); // add all non-default graphs to default graph return api.mergeNodeMaps(graphs); }; /** * Recursively flattens the subjects in the given JSON-LD expanded input * into a node map. * * @param input the JSON-LD expanded input. * @param graphs a map of graph name to subject map. * @param graph the name of the current graph. * @param issuer the blank node identifier issuer. * @param name the name assigned to the current input if it is a bnode. * @param list the list to append to, null for none. */ api.createNodeMap = (input, graphs, graph, issuer, name, list) => { // recurse through array if(types.isArray(input)) { for(const node of input) { api.createNodeMap(node, graphs, graph, issuer, undefined, list); } return; } // add non-object to list if(!types.isObject(input)) { if(list) { list.push(input); } return; } // add values to list if(graphTypes.isValue(input)) { if('@type' in input) { let type = input['@type']; // rename @type blank node if(type.indexOf('_:') === 0) { input['@type'] = type = issuer.getId(type); } } if(list) { list.push(input); } return; } else if(list && graphTypes.isList(input)) { const _list = []; api.createNodeMap(input['@list'], graphs, graph, issuer, name, _list); list.push({'@list': _list}); return; } // Note: At this point, input must be a subject. // spec requires @type to be named first, so assign names early if('@type' in input) { const types = input['@type']; for(const type of types) { if(type.indexOf('_:') === 0) { issuer.getId(type); } } } // get name for subject if(types.isUndefined(name)) { name = graphTypes.isBlankNode(input) ? issuer.getId(input['@id']) : input['@id']; } // add subject reference to list if(list) { list.push({'@id': name}); } // create new subject or merge into existing one const subjects = graphs[graph]; const subject = subjects[name] = subjects[name] || {}; subject['@id'] = name; const properties = Object.keys(input).sort(); for(let property of properties) { // skip @id if(property === '@id') { continue; } // handle reverse properties if(property === '@reverse') { const referencedNode = {'@id': name}; const reverseMap = input['@reverse']; for(const reverseProperty in reverseMap) { const items = reverseMap[reverseProperty]; for(const item of items) { let itemName = item['@id']; if(graphTypes.isBlankNode(item)) { itemName = issuer.getId(itemName); } api.createNodeMap(item, graphs, graph, issuer, itemName); util.addValue( subjects[itemName], reverseProperty, referencedNode, {propertyIsArray: true, allowDuplicate: false}); } } continue; } // recurse into graph if(property === '@graph') { // add graph subjects map entry if(!(name in graphs)) { graphs[name] = {}; } api.createNodeMap(input[property], graphs, name, issuer); continue; } // recurse into included if(property === '@included') { api.createNodeMap(input[property], graphs, graph, issuer); continue; } // copy non-@type keywords if(property !== '@type' && isKeyword(property)) { if(property === '@index' && property in subject && (input[property] !== subject[property] || input[property]['@id'] !== subject[property]['@id'])) { throw new JsonLdError( 'Invalid JSON-LD syntax; conflicting @index property detected.', 'jsonld.SyntaxError', {code: 'conflicting indexes', subject}); } subject[property] = input[property]; continue; } // iterate over objects const objects = input[property]; // if property is a bnode, assign it a new id if(property.indexOf('_:') === 0) { property = issuer.getId(property); } // ensure property is added for empty arrays if(objects.length === 0) { util.addValue(subject, property, [], {propertyIsArray: true}); continue; } for(let o of objects) { if(property === '@type') { // rename @type blank nodes o = (o.indexOf('_:') === 0) ? issuer.getId(o) : o; } // handle embedded subject or subject reference if(graphTypes.isSubject(o) || graphTypes.isSubjectReference(o)) { // skip null @id if('@id' in o && !o['@id']) { continue; } // relabel blank node @id const id = graphTypes.isBlankNode(o) ? issuer.getId(o['@id']) : o['@id']; // add reference and recurse util.addValue( subject, property, {'@id': id}, {propertyIsArray: true, allowDuplicate: false}); api.createNodeMap(o, graphs, graph, issuer, id); } else if(graphTypes.isValue(o)) { util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } else if(graphTypes.isList(o)) { // handle @list const _list = []; api.createNodeMap(o['@list'], graphs, graph, issuer, name, _list); o = {'@list': _list}; util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } else { // handle @value api.createNodeMap(o, graphs, graph, issuer, name); util.addValue( subject, property, o, {propertyIsArray: true, allowDuplicate: false}); } } } }; /** * Merge separate named graphs into a single merged graph including * all nodes from the default graph and named graphs. * * @param graphs a map of graph name to subject map. * * @return the merged graph map. */ api.mergeNodeMapGraphs = graphs => { const merged = {}; for(const name of Object.keys(graphs).sort()) { for(const id of Object.keys(graphs[name]).sort()) { const node = graphs[name][id]; if(!(id in merged)) { merged[id] = {'@id': id}; } const mergedNode = merged[id]; for(const property of Object.keys(node).sort()) { if(isKeyword(property) && property !== '@type') { // copy keywords mergedNode[property] = util.clone(node[property]); } else { // merge objects for(const value of node[property]) { util.addValue( mergedNode, property, util.clone(value), {propertyIsArray: true, allowDuplicate: false}); } } } } } return merged; }; api.mergeNodeMaps = graphs => { // add all non-default graphs to default graph const defaultGraph = graphs['@default']; const graphNames = Object.keys(graphs).sort(); for(const graphName of graphNames) { if(graphName === '@default') { continue; } const nodeMap = graphs[graphName]; let subject = defaultGraph[graphName]; if(!subject) { defaultGraph[graphName] = subject = { '@id': graphName, '@graph': [] }; } else if(!('@graph' in subject)) { subject['@graph'] = []; } const graph = subject['@graph']; for(const id of Object.keys(nodeMap).sort()) { const node = nodeMap[id]; // only add full subjects if(!graphTypes.isSubjectReference(node)) { graph.push(node); } } } return defaultGraph; }; jsonld.js-4.0.1/lib/toRdf.js000066400000000000000000000173461401135163200156110ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const {createNodeMap} = require('./nodeMap'); const {isKeyword} = require('./context'); const graphTypes = require('./graphTypes'); const jsonCanonicalize = require('canonicalize'); const types = require('./types'); const util = require('./util'); const { // RDF, // RDF_LIST, RDF_FIRST, RDF_REST, RDF_NIL, RDF_TYPE, // RDF_PLAIN_LITERAL, // RDF_XML_LITERAL, RDF_JSON_LITERAL, // RDF_OBJECT, RDF_LANGSTRING, // XSD, XSD_BOOLEAN, XSD_DOUBLE, XSD_INTEGER, XSD_STRING, } = require('./constants'); const { isAbsolute: _isAbsoluteIri } = require('./url'); const api = {}; module.exports = api; /** * Outputs an RDF dataset for the expanded JSON-LD input. * * @param input the expanded JSON-LD input. * @param options the RDF serialization options. * * @return the RDF dataset. */ api.toRDF = (input, options) => { // create node map for default graph (and any named graphs) const issuer = new util.IdentifierIssuer('_:b'); const nodeMap = {'@default': {}}; createNodeMap(input, nodeMap, '@default', issuer); const dataset = []; const graphNames = Object.keys(nodeMap).sort(); for(const graphName of graphNames) { let graphTerm; if(graphName === '@default') { graphTerm = {termType: 'DefaultGraph', value: ''}; } else if(_isAbsoluteIri(graphName)) { if(graphName.startsWith('_:')) { graphTerm = {termType: 'BlankNode'}; } else { graphTerm = {termType: 'NamedNode'}; } graphTerm.value = graphName; } else { // skip relative IRIs (not valid RDF) continue; } _graphToRDF(dataset, nodeMap[graphName], graphTerm, issuer, options); } return dataset; }; /** * Adds RDF quads for a particular graph to the given dataset. * * @param dataset the dataset to append RDF quads to. * @param graph the graph to create RDF quads for. * @param graphTerm the graph term for each quad. * @param issuer a IdentifierIssuer for assigning blank node names. * @param options the RDF serialization options. * * @return the array of RDF triples for the given graph. */ function _graphToRDF(dataset, graph, graphTerm, issuer, options) { const ids = Object.keys(graph).sort(); for(const id of ids) { const node = graph[id]; const properties = Object.keys(node).sort(); for(let property of properties) { const items = node[property]; if(property === '@type') { property = RDF_TYPE; } else if(isKeyword(property)) { continue; } for(const item of items) { // RDF subject const subject = { termType: id.startsWith('_:') ? 'BlankNode' : 'NamedNode', value: id }; // skip relative IRI subjects (not valid RDF) if(!_isAbsoluteIri(id)) { continue; } // RDF predicate const predicate = { termType: property.startsWith('_:') ? 'BlankNode' : 'NamedNode', value: property }; // skip relative IRI predicates (not valid RDF) if(!_isAbsoluteIri(property)) { continue; } // skip blank node predicates unless producing generalized RDF if(predicate.termType === 'BlankNode' && !options.produceGeneralizedRdf) { continue; } // convert list, value or node object to triple const object = _objectToRDF(item, issuer, dataset, graphTerm, options.rdfDirection); // skip null objects (they are relative IRIs) if(object) { dataset.push({ subject, predicate, object, graph: graphTerm }); } } } } } /** * Converts a @list value into linked list of blank node RDF quads * (an RDF collection). * * @param list the @list value. * @param issuer a IdentifierIssuer for assigning blank node names. * @param dataset the array of quads to append to. * @param graphTerm the graph term for each quad. * * @return the head of the list. */ function _listToRDF(list, issuer, dataset, graphTerm, rdfDirection) { const first = {termType: 'NamedNode', value: RDF_FIRST}; const rest = {termType: 'NamedNode', value: RDF_REST}; const nil = {termType: 'NamedNode', value: RDF_NIL}; const last = list.pop(); // Result is the head of the list const result = last ? {termType: 'BlankNode', value: issuer.getId()} : nil; let subject = result; for(const item of list) { const object = _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection); const next = {termType: 'BlankNode', value: issuer.getId()}; dataset.push({ subject, predicate: first, object, graph: graphTerm }); dataset.push({ subject, predicate: rest, object: next, graph: graphTerm }); subject = next; } // Tail of list if(last) { const object = _objectToRDF(last, issuer, dataset, graphTerm, rdfDirection); dataset.push({ subject, predicate: first, object, graph: graphTerm }); dataset.push({ subject, predicate: rest, object: nil, graph: graphTerm }); } return result; } /** * Converts a JSON-LD value object to an RDF literal or a JSON-LD string, * node object to an RDF resource, or adds a list. * * @param item the JSON-LD value or node object. * @param issuer a IdentifierIssuer for assigning blank node names. * @param dataset the dataset to append RDF quads to. * @param graphTerm the graph term for each quad. * * @return the RDF literal or RDF resource. */ function _objectToRDF(item, issuer, dataset, graphTerm, rdfDirection) { const object = {}; // convert value object to RDF if(graphTypes.isValue(item)) { object.termType = 'Literal'; object.value = undefined; object.datatype = { termType: 'NamedNode' }; let value = item['@value']; const datatype = item['@type'] || null; // convert to XSD/JSON datatypes as appropriate if(datatype === '@json') { object.value = jsonCanonicalize(value); object.datatype.value = RDF_JSON_LITERAL; } else if(types.isBoolean(value)) { object.value = value.toString(); object.datatype.value = datatype || XSD_BOOLEAN; } else if(types.isDouble(value) || datatype === XSD_DOUBLE) { if(!types.isDouble(value)) { value = parseFloat(value); } // canonical double representation object.value = value.toExponential(15).replace(/(\d)0*e\+?/, '$1E'); object.datatype.value = datatype || XSD_DOUBLE; } else if(types.isNumber(value)) { object.value = value.toFixed(0); object.datatype.value = datatype || XSD_INTEGER; } else if(rdfDirection === 'i18n-datatype' && '@direction' in item) { const datatype = 'https://www.w3.org/ns/i18n#' + (item['@language'] || '') + `_${item['@direction']}`; object.datatype.value = datatype; object.value = value; } else if('@language' in item) { object.value = value; object.datatype.value = datatype || RDF_LANGSTRING; object.language = item['@language']; } else { object.value = value; object.datatype.value = datatype || XSD_STRING; } } else if(graphTypes.isList(item)) { const _list = _listToRDF(item['@list'], issuer, dataset, graphTerm, rdfDirection); object.termType = _list.termType; object.value = _list.value; } else { // convert string/node object to RDF const id = types.isObject(item) ? item['@id'] : item; object.termType = id.startsWith('_:') ? 'BlankNode' : 'NamedNode'; object.value = id; } // skip relative IRIs, not valid RDF if(object.termType === 'NamedNode' && !_isAbsoluteIri(object.value)) { return null; } return object; } jsonld.js-4.0.1/lib/types.js000066400000000000000000000042361401135163200156710ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const api = {}; module.exports = api; /** * Returns true if the given value is an Array. * * @param v the value to check. * * @return true if the value is an Array, false if not. */ api.isArray = Array.isArray; /** * Returns true if the given value is a Boolean. * * @param v the value to check. * * @return true if the value is a Boolean, false if not. */ api.isBoolean = v => (typeof v === 'boolean' || Object.prototype.toString.call(v) === '[object Boolean]'); /** * Returns true if the given value is a double. * * @param v the value to check. * * @return true if the value is a double, false if not. */ api.isDouble = v => api.isNumber(v) && (String(v).indexOf('.') !== -1 || Math.abs(v) >= 1e21); /** * Returns true if the given value is an empty Object. * * @param v the value to check. * * @return true if the value is an empty Object, false if not. */ api.isEmptyObject = v => api.isObject(v) && Object.keys(v).length === 0; /** * Returns true if the given value is a Number. * * @param v the value to check. * * @return true if the value is a Number, false if not. */ api.isNumber = v => (typeof v === 'number' || Object.prototype.toString.call(v) === '[object Number]'); /** * Returns true if the given value is numeric. * * @param v the value to check. * * @return true if the value is numeric, false if not. */ api.isNumeric = v => !isNaN(parseFloat(v)) && isFinite(v); /** * Returns true if the given value is an Object. * * @param v the value to check. * * @return true if the value is an Object, false if not. */ api.isObject = v => Object.prototype.toString.call(v) === '[object Object]'; /** * Returns true if the given value is a String. * * @param v the value to check. * * @return true if the value is a String, false if not. */ api.isString = v => (typeof v === 'string' || Object.prototype.toString.call(v) === '[object String]'); /** * Returns true if the given value is undefined. * * @param v the value to check. * * @return true if the value is undefined, false if not. */ api.isUndefined = v => typeof v === 'undefined'; jsonld.js-4.0.1/lib/url.js000066400000000000000000000162451401135163200153320ustar00rootroot00000000000000/* * Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const types = require('./types'); const api = {}; module.exports = api; // define URL parser // parseUri 1.2.2 // (c) Steven Levithan // MIT License // with local jsonld.js modifications api.parsers = { simple: { // RFC 3986 basic parts keys: [ 'href', 'scheme', 'authority', 'path', 'query', 'fragment' ], /* eslint-disable-next-line max-len */ regex: /^(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?/ }, full: { keys: [ 'href', 'protocol', 'scheme', 'authority', 'auth', 'user', 'password', 'hostname', 'port', 'path', 'directory', 'file', 'query', 'fragment' ], /* eslint-disable-next-line max-len */ regex: /^(([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?(?:(((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/ } }; api.parse = (str, parser) => { const parsed = {}; const o = api.parsers[parser || 'full']; const m = o.regex.exec(str); let i = o.keys.length; while(i--) { parsed[o.keys[i]] = (m[i] === undefined) ? null : m[i]; } // remove default ports in found in URLs if((parsed.scheme === 'https' && parsed.port === '443') || (parsed.scheme === 'http' && parsed.port === '80')) { parsed.href = parsed.href.replace(':' + parsed.port, ''); parsed.authority = parsed.authority.replace(':' + parsed.port, ''); parsed.port = null; } parsed.normalizedPath = api.removeDotSegments(parsed.path); return parsed; }; /** * Prepends a base IRI to the given relative IRI. * * @param base the base IRI. * @param iri the relative IRI. * * @return the absolute IRI. */ api.prependBase = (base, iri) => { // skip IRI processing if(base === null) { return iri; } // already an absolute IRI if(api.isAbsolute(iri)) { return iri; } // parse base if it is a string if(!base || types.isString(base)) { base = api.parse(base || ''); } // parse given IRI const rel = api.parse(iri); // per RFC3986 5.2.2 const transform = { protocol: base.protocol || '' }; if(rel.authority !== null) { transform.authority = rel.authority; transform.path = rel.path; transform.query = rel.query; } else { transform.authority = base.authority; if(rel.path === '') { transform.path = base.path; if(rel.query !== null) { transform.query = rel.query; } else { transform.query = base.query; } } else { if(rel.path.indexOf('/') === 0) { // IRI represents an absolute path transform.path = rel.path; } else { // merge paths let path = base.path; // append relative path to the end of the last directory from base path = path.substr(0, path.lastIndexOf('/') + 1); if((path.length > 0 || base.authority) && path.substr(-1) !== '/') { path += '/'; } path += rel.path; transform.path = path; } transform.query = rel.query; } } if(rel.path !== '') { // remove slashes and dots in path transform.path = api.removeDotSegments(transform.path); } // construct URL let rval = transform.protocol; if(transform.authority !== null) { rval += '//' + transform.authority; } rval += transform.path; if(transform.query !== null) { rval += '?' + transform.query; } if(rel.fragment !== null) { rval += '#' + rel.fragment; } // handle empty base if(rval === '') { rval = './'; } return rval; }; /** * Removes a base IRI from the given absolute IRI. * * @param base the base IRI. * @param iri the absolute IRI. * * @return the relative IRI if relative to base, otherwise the absolute IRI. */ api.removeBase = (base, iri) => { // skip IRI processing if(base === null) { return iri; } if(!base || types.isString(base)) { base = api.parse(base || ''); } // establish base root let root = ''; if(base.href !== '') { root += (base.protocol || '') + '//' + (base.authority || ''); } else if(iri.indexOf('//')) { // support network-path reference with empty base root += '//'; } // IRI not relative to base if(iri.indexOf(root) !== 0) { return iri; } // remove root from IRI and parse remainder const rel = api.parse(iri.substr(root.length)); // remove path segments that match (do not remove last segment unless there // is a hash or query) const baseSegments = base.normalizedPath.split('/'); const iriSegments = rel.normalizedPath.split('/'); const last = (rel.fragment || rel.query) ? 0 : 1; while(baseSegments.length > 0 && iriSegments.length > last) { if(baseSegments[0] !== iriSegments[0]) { break; } baseSegments.shift(); iriSegments.shift(); } // use '../' for each non-matching base segment let rval = ''; if(baseSegments.length > 0) { // don't count the last segment (if it ends with '/' last path doesn't // count and if it doesn't end with '/' it isn't a path) baseSegments.pop(); for(let i = 0; i < baseSegments.length; ++i) { rval += '../'; } } // prepend remaining segments rval += iriSegments.join('/'); // add query and hash if(rel.query !== null) { rval += '?' + rel.query; } if(rel.fragment !== null) { rval += '#' + rel.fragment; } // handle empty base if(rval === '') { rval = './'; } return rval; }; /** * Removes dot segments from a URL path. * * @param path the path to remove dot segments from. */ api.removeDotSegments = path => { // RFC 3986 5.2.4 (reworked) // empty path shortcut if(path.length === 0) { return ''; } const input = path.split('/'); const output = []; while(input.length > 0) { const next = input.shift(); const done = input.length === 0; if(next === '.') { if(done) { // ensure output has trailing / output.push(''); } continue; } if(next === '..') { output.pop(); if(done) { // ensure output has trailing / output.push(''); } continue; } output.push(next); } // if path was absolute, ensure output has leading / if(path[0] === '/' && output.length > 0 && output[0] !== '') { output.unshift(''); } if(output.length === 1 && output[0] === '') { return '/'; } return output.join('/'); }; // TODO: time better isAbsolute/isRelative checks using full regexes: // http://jmrware.com/articles/2009/uri_regexp/URI_regex.html // regex to check for absolute IRI (starting scheme and ':') or blank node IRI const isAbsoluteRegex = /^([A-Za-z][A-Za-z0-9+-.]*|_):[^\s]*$/; /** * Returns true if the given value is an absolute IRI or blank node IRI, false * if not. * Note: This weak check only checks for a correct starting scheme. * * @param v the value to check. * * @return true if the value is an absolute IRI, false if not. */ api.isAbsolute = v => types.isString(v) && isAbsoluteRegex.test(v); /** * Returns true if the given value is a relative IRI, false if not. * Note: this is a weak check. * * @param v the value to check. * * @return true if the value is a relative IRI, false if not. */ api.isRelative = v => types.isString(v); jsonld.js-4.0.1/lib/util.js000066400000000000000000000302761401135163200155050ustar00rootroot00000000000000/* * Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved. */ 'use strict'; const graphTypes = require('./graphTypes'); const types = require('./types'); // TODO: move `IdentifierIssuer` to its own package const IdentifierIssuer = require('rdf-canonize').IdentifierIssuer; const JsonLdError = require('./JsonLdError'); // constants const REGEX_LINK_HEADERS = /(?:<[^>]*?>|"[^"]*?"|[^,])+/g; const REGEX_LINK_HEADER = /\s*<([^>]*?)>\s*(?:;\s*(.*))?/; const REGEX_LINK_HEADER_PARAMS = /(.*?)=(?:(?:"([^"]*?)")|([^"]*?))\s*(?:(?:;\s*)|$)/g; const DEFAULTS = { headers: { accept: 'application/ld+json, application/json' } }; const api = {}; module.exports = api; api.IdentifierIssuer = IdentifierIssuer; /** * Clones an object, array, Map, Set, or string/number. If a typed JavaScript * object is given, such as a Date, it will be converted to a string. * * @param value the value to clone. * * @return the cloned value. */ api.clone = function(value) { if(value && typeof value === 'object') { let rval; if(types.isArray(value)) { rval = []; for(let i = 0; i < value.length; ++i) { rval[i] = api.clone(value[i]); } } else if(value instanceof Map) { rval = new Map(); for(const [k, v] of value) { rval.set(k, api.clone(v)); } } else if(value instanceof Set) { rval = new Set(); for(const v of value) { rval.add(api.clone(v)); } } else if(types.isObject(value)) { rval = {}; for(const key in value) { rval[key] = api.clone(value[key]); } } else { rval = value.toString(); } return rval; } return value; }; /** * Ensure a value is an array. If the value is an array, it is returned. * Otherwise, it is wrapped in an array. * * @param value the value to return as an array. * * @return the value as an array. */ api.asArray = function(value) { return Array.isArray(value) ? value : [value]; }; /** * Builds an HTTP headers object for making a JSON-LD request from custom * headers and asserts the `accept` header isn't overridden. * * @param headers an object of headers with keys as header names and values * as header values. * * @return an object of headers with a valid `accept` header. */ api.buildHeaders = (headers = {}) => { const hasAccept = Object.keys(headers).some( h => h.toLowerCase() === 'accept'); if(hasAccept) { throw new RangeError( 'Accept header may not be specified; only "' + DEFAULTS.headers.accept + '" is supported.'); } return Object.assign({Accept: DEFAULTS.headers.accept}, headers); }; /** * Parses a link header. The results will be key'd by the value of "rel". * * Link: ; * rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json" * * Parses as: { * 'http://www.w3.org/ns/json-ld#context': { * target: http://json-ld.org/contexts/person.jsonld, * type: 'application/ld+json' * } * } * * If there is more than one "rel" with the same IRI, then entries in the * resulting map for that "rel" will be arrays. * * @param header the link header to parse. */ api.parseLinkHeader = header => { const rval = {}; // split on unbracketed/unquoted commas const entries = header.match(REGEX_LINK_HEADERS); for(let i = 0; i < entries.length; ++i) { let match = entries[i].match(REGEX_LINK_HEADER); if(!match) { continue; } const result = {target: match[1]}; const params = match[2]; while((match = REGEX_LINK_HEADER_PARAMS.exec(params))) { result[match[1]] = (match[2] === undefined) ? match[3] : match[2]; } const rel = result['rel'] || ''; if(Array.isArray(rval[rel])) { rval[rel].push(result); } else if(rval.hasOwnProperty(rel)) { rval[rel] = [rval[rel], result]; } else { rval[rel] = result; } } return rval; }; /** * Throws an exception if the given value is not a valid @type value. * * @param v the value to check. */ api.validateTypeValue = (v, isFrame) => { if(types.isString(v)) { return; } if(types.isArray(v) && v.every(vv => types.isString(vv))) { return; } if(isFrame && types.isObject(v)) { switch(Object.keys(v).length) { case 0: // empty object is wildcard return; case 1: // default entry is all strings if('@default' in v && api.asArray(v['@default']).every(vv => types.isString(vv))) { return; } } } throw new JsonLdError( 'Invalid JSON-LD syntax; "@type" value must a string, an array of ' + 'strings, an empty object, ' + 'or a default object.', 'jsonld.SyntaxError', {code: 'invalid type value', value: v}); }; /** * Returns true if the given subject has the given property. * * @param subject the subject to check. * @param property the property to look for. * * @return true if the subject has the given property, false if not. */ api.hasProperty = (subject, property) => { if(subject.hasOwnProperty(property)) { const value = subject[property]; return (!types.isArray(value) || value.length > 0); } return false; }; /** * Determines if the given value is a property of the given subject. * * @param subject the subject to check. * @param property the property to check. * @param value the value to check. * * @return true if the value exists, false if not. */ api.hasValue = (subject, property, value) => { if(api.hasProperty(subject, property)) { let val = subject[property]; const isList = graphTypes.isList(val); if(types.isArray(val) || isList) { if(isList) { val = val['@list']; } for(let i = 0; i < val.length; ++i) { if(api.compareValues(value, val[i])) { return true; } } } else if(!types.isArray(value)) { // avoid matching the set of values with an array value parameter return api.compareValues(value, val); } } return false; }; /** * Adds a value to a subject. If the value is an array, all values in the * array will be added. * * @param subject the subject to add the value to. * @param property the property that relates the value to the subject. * @param value the value to add. * @param [options] the options to use: * [propertyIsArray] true if the property is always an array, false * if not (default: false). * [valueIsArray] true if the value to be added should be preserved as * an array (lists) (default: false). * [allowDuplicate] true to allow duplicates, false not to (uses a * simple shallow comparison of subject ID or value) (default: true). * [prependValue] false to prepend value to any existing values. * (default: false) */ api.addValue = (subject, property, value, options) => { options = options || {}; if(!('propertyIsArray' in options)) { options.propertyIsArray = false; } if(!('valueIsArray' in options)) { options.valueIsArray = false; } if(!('allowDuplicate' in options)) { options.allowDuplicate = true; } if(!('prependValue' in options)) { options.prependValue = false; } if(options.valueIsArray) { subject[property] = value; } else if(types.isArray(value)) { if(value.length === 0 && options.propertyIsArray && !subject.hasOwnProperty(property)) { subject[property] = []; } if(options.prependValue) { value = value.concat(subject[property]); subject[property] = []; } for(let i = 0; i < value.length; ++i) { api.addValue(subject, property, value[i], options); } } else if(subject.hasOwnProperty(property)) { // check if subject already has value if duplicates not allowed const hasValue = (!options.allowDuplicate && api.hasValue(subject, property, value)); // make property an array if value not present or always an array if(!types.isArray(subject[property]) && (!hasValue || options.propertyIsArray)) { subject[property] = [subject[property]]; } // add new value if(!hasValue) { if(options.prependValue) { subject[property].unshift(value); } else { subject[property].push(value); } } } else { // add new value as set or single value subject[property] = options.propertyIsArray ? [value] : value; } }; /** * Gets all of the values for a subject's property as an array. * * @param subject the subject. * @param property the property. * * @return all of the values for a subject's property as an array. */ api.getValues = (subject, property) => [].concat(subject[property] || []); /** * Removes a property from a subject. * * @param subject the subject. * @param property the property. */ api.removeProperty = (subject, property) => { delete subject[property]; }; /** * Removes a value from a subject. * * @param subject the subject. * @param property the property that relates the value to the subject. * @param value the value to remove. * @param [options] the options to use: * [propertyIsArray] true if the property is always an array, false * if not (default: false). */ api.removeValue = (subject, property, value, options) => { options = options || {}; if(!('propertyIsArray' in options)) { options.propertyIsArray = false; } // filter out value const values = api.getValues(subject, property).filter( e => !api.compareValues(e, value)); if(values.length === 0) { api.removeProperty(subject, property); } else if(values.length === 1 && !options.propertyIsArray) { subject[property] = values[0]; } else { subject[property] = values; } }; /** * Relabels all blank nodes in the given JSON-LD input. * * @param input the JSON-LD input. * @param [options] the options to use: * [issuer] an IdentifierIssuer to use to label blank nodes. */ api.relabelBlankNodes = (input, options) => { options = options || {}; const issuer = options.issuer || new IdentifierIssuer('_:b'); return _labelBlankNodes(issuer, input); }; /** * Compares two JSON-LD values for equality. Two JSON-LD values will be * considered equal if: * * 1. They are both primitives of the same type and value. * 2. They are both @values with the same @value, @type, @language, * and @index, OR * 3. They both have @ids they are the same. * * @param v1 the first value. * @param v2 the second value. * * @return true if v1 and v2 are considered equal, false if not. */ api.compareValues = (v1, v2) => { // 1. equal primitives if(v1 === v2) { return true; } // 2. equal @values if(graphTypes.isValue(v1) && graphTypes.isValue(v2) && v1['@value'] === v2['@value'] && v1['@type'] === v2['@type'] && v1['@language'] === v2['@language'] && v1['@index'] === v2['@index']) { return true; } // 3. equal @ids if(types.isObject(v1) && ('@id' in v1) && types.isObject(v2) && ('@id' in v2)) { return v1['@id'] === v2['@id']; } return false; }; /** * Compares two strings first based on length and then lexicographically. * * @param a the first string. * @param b the second string. * * @return -1 if a < b, 1 if a > b, 0 if a === b. */ api.compareShortestLeast = (a, b) => { if(a.length < b.length) { return -1; } if(b.length < a.length) { return 1; } if(a === b) { return 0; } return (a < b) ? -1 : 1; }; /** * Labels the blank nodes in the given value using the given IdentifierIssuer. * * @param issuer the IdentifierIssuer to use. * @param element the element with blank nodes to rename. * * @return the element. */ function _labelBlankNodes(issuer, element) { if(types.isArray(element)) { for(let i = 0; i < element.length; ++i) { element[i] = _labelBlankNodes(issuer, element[i]); } } else if(graphTypes.isList(element)) { element['@list'] = _labelBlankNodes(issuer, element['@list']); } else if(types.isObject(element)) { // relabel blank node if(graphTypes.isBlankNode(element)) { element['@id'] = issuer.getId(element['@id']); } // recursively apply to all keys const keys = Object.keys(element).sort(); for(let ki = 0; ki < keys.length; ++ki) { const key = keys[ki]; if(key !== '@id') { element[key] = _labelBlankNodes(issuer, element[key]); } } } return element; } jsonld.js-4.0.1/package.json000066400000000000000000000105671401135163200157130ustar00rootroot00000000000000{ "name": "jsonld", "version": "4.0.1", "description": "A JSON-LD Processor and API implementation in JavaScript.", "homepage": "https://github.com/digitalbazaar/jsonld.js", "author": { "name": "Digital Bazaar, Inc.", "email": "support@digitalbazaar.com", "url": "https://digitalbazaar.com/" }, "contributors": [ "Dave Longley ", "David I. Lehn " ], "repository": { "type": "git", "url": "https://github.com/digitalbazaar/jsonld.js" }, "bugs": { "url": "https://github.com/digitalbazaar/jsonld.js/issues", "email": "support@digitalbazaar.com" }, "license": "BSD-3-Clause", "main": "lib/index.js", "files": [ "dist/*.js", "dist/*.js.map", "dist/node6/**/*.js", "lib/*.js", "lib/**/*.js" ], "dependencies": { "canonicalize": "^1.0.1", "lru-cache": "^5.1.1", "object.fromentries": "^2.0.2", "rdf-canonize": "^2.0.1", "request": "^2.88.0", "semver": "^6.3.0" }, "devDependencies": { "@babel/cli": "^7.7.5", "@babel/core": "^7.7.5", "@babel/plugin-proposal-object-rest-spread": "^7.7.4", "@babel/plugin-transform-modules-commonjs": "^7.7.5", "@babel/plugin-transform-runtime": "^7.7.5", "@babel/preset-env": "^7.7.5", "@babel/register": "^7.7.4", "@babel/runtime": "^7.7.5", "babel-loader": "^8.0.5", "benchmark": "^2.1.4", "browserify": "^16.2.3", "chai": "^4.2.0", "commander": "^2.19.0", "core-js": "3.x", "cors": "^2.7.1", "cross-env": "^5.2.0", "eslint": "^6.8.0", "eslint-config-digitalbazaar": "2.5.x", "express": "^4.16.4", "fs-extra": "^8.1.0", "join-path-js": "0.0.0", "karma": "^3.1.1", "karma-babel-preprocessor": "^8.0.0", "karma-browserify": "^6.0.0", "karma-chrome-launcher": "^2.2.0", "karma-edge-launcher": "^0.4.2", "karma-firefox-launcher": "^1.1.0", "karma-ie-launcher": "^1.0.0", "karma-mocha": "^1.3.0", "karma-mocha-reporter": "^2.2.5", "karma-safari-launcher": "^1.0.0", "karma-server-side": "^1.7.0", "karma-sourcemap-loader": "^0.3.7", "karma-tap-reporter": "0.0.6", "karma-webpack": "^3.0.5", "mocha": "^6.2.2", "mocha-lcov-reporter": "^1.3.0", "nyc": "^14.1.1", "webpack": "^4.29.5", "webpack-cli": "^3.2.3", "webpack-merge": "^4.2.1" }, "engines": { "node": ">=6" }, "keywords": [ "JSON", "JSON-LD", "Linked Data", "RDF", "Semantic Web", "jsonld" ], "scripts": { "prepublish": "npm run build", "build": "npm run build-webpack && npm run build-node6", "build-webpack": "webpack", "build-node6": "BROWSERSLIST='node 6' babel --no-babelrc lib --out-dir dist/node6/lib --presets=@babel/preset-env", "fetch-test-suites": "npm run fetch-json-ld-wg-test-suite && npm run fetch-json-ld-org-test-suite && npm run fetch-normalization-test-suite", "fetch-json-ld-wg-test-suite": "npm run fetch-json-ld-api-test-suite && npm run fetch-json-ld-framing-test-suite", "fetch-json-ld-api-test-suite": "if [ ! -e test-suites/json-wg-api ]; then git clone --depth 1 https://github.com/w3c/json-ld-api.git test-suites/json-ld-api; fi", "fetch-json-ld-framing-test-suite": "if [ ! -e test-suites/json-wg-framing ]; then git clone --depth 1 https://github.com/w3c/json-ld-framing.git test-suites/json-ld-framing; fi", "fetch-json-ld-org-test-suite": "if [ ! -e test-suites/json-ld.org ]; then git clone --depth 1 https://github.com/json-ld/json-ld.org.git test-suites/json-ld.org; fi", "fetch-normalization-test-suite": "if [ ! -e test-suites/normalization ]; then git clone --depth 1 https://github.com/json-ld/normalization.git test-suites/normalization; fi", "test": "npm run test-node", "test-node": "cross-env NODE_ENV=test mocha --delay -t 30000 -A -R ${REPORTER:-spec} tests/test.js", "test-karma": "cross-env NODE_ENV=test karma start", "coverage": "nyc --reporter=lcov --reporter=text-summary npm test", "coverage-ci": "nyc --reporter=lcovonly npm run test", "coverage-report": "nyc report", "lint": "eslint *.js lib/**.js tests/**.js" }, "nyc": { "exclude": [ "lib/documentLoaders/xhr.js", "tests" ] }, "browser": { "lib/index.js": "./lib/jsonld.js", "crypto": false, "http": false, "jsonld-request": false, "request": false, "url": false, "util": false } } jsonld.js-4.0.1/tests/000077500000000000000000000000001401135163200145565ustar00rootroot00000000000000jsonld.js-4.0.1/tests/.eslintrc.js000066400000000000000000000000631401135163200170140ustar00rootroot00000000000000module.exports = { env: { mocha: true } }; jsonld.js-4.0.1/tests/contexts/000077500000000000000000000000001401135163200164255ustar00rootroot00000000000000jsonld.js-4.0.1/tests/contexts/context-0008.jsonld000066400000000000000000000001261401135163200217100ustar00rootroot00000000000000{ "@context": [ { "t1": "ex:t1" }, { "t1": null } ] } jsonld.js-4.0.1/tests/contexts/context-1.jsonld000066400000000000000000000000771401135163200214660ustar00rootroot00000000000000{ "@context": { "term1": "http://example.org/term1" } }jsonld.js-4.0.1/tests/contexts/context-2.jsonld000066400000000000000000000000721401135163200214620ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-1.jsonld" }jsonld.js-4.0.1/tests/contexts/context-3.jsonld000066400000000000000000000000721401135163200214630ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-2.jsonld" }jsonld.js-4.0.1/tests/contexts/context-4.jsonld000066400000000000000000000001421401135163200214620ustar00rootroot00000000000000{ "@context": [ "/context-1.jsonld", "/context-2.jsonld", "/context-3.jsonld" ] }jsonld.js-4.0.1/tests/contexts/context-5.jsonld000066400000000000000000000000441401135163200214640ustar00rootroot00000000000000{ "@context": "context-1.jsonld" }jsonld.js-4.0.1/tests/earl-report.js000066400000000000000000000064321401135163200173550ustar00rootroot00000000000000/** * EARL Report * * @author Dave Longley * * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. */ /** * Create an EARL Reporter. * * @param options {Object} reporter options * id: {String} report id */ function EarlReport(options) { let today = new Date(); today = today.getFullYear() + '-' + (today.getMonth() < 9 ? '0' + (today.getMonth() + 1) : today.getMonth() + 1) + '-' + (today.getDate() < 10 ? '0' + today.getDate() : today.getDate()); // one date for tests with no subsecond resolution this.now = new Date(); this.now.setMilliseconds(0); this.id = options.id; /* eslint-disable quote-props */ this._report = { '@context': { 'doap': 'http://usefulinc.com/ns/doap#', 'foaf': 'http://xmlns.com/foaf/0.1/', 'dc': 'http://purl.org/dc/terms/', 'earl': 'http://www.w3.org/ns/earl#', 'xsd': 'http://www.w3.org/2001/XMLSchema#', 'doap:homepage': {'@type': '@id'}, 'doap:license': {'@type': '@id'}, 'dc:creator': {'@type': '@id'}, 'foaf:homepage': {'@type': '@id'}, 'subjectOf': {'@reverse': 'earl:subject'}, 'earl:assertedBy': {'@type': '@id'}, 'earl:mode': {'@type': '@id'}, 'earl:test': {'@type': '@id'}, 'earl:outcome': {'@type': '@id'}, 'dc:date': {'@type': 'xsd:date'}, 'doap:created': {'@type': 'xsd:date'} }, '@id': 'https://github.com/digitalbazaar/jsonld.js', '@type': [ 'doap:Project', 'earl:TestSubject', 'earl:Software' ], 'doap:name': 'jsonld.js', 'dc:title': 'jsonld.js', 'doap:homepage': 'https://github.com/digitalbazaar/jsonld.js', 'doap:license': 'https://github.com/digitalbazaar/jsonld.js/blob/master/LICENSE', 'doap:description': 'A JSON-LD processor for JavaScript', 'doap:programming-language': 'JavaScript', 'dc:creator': 'https://github.com/dlongley', 'doap:developer': { '@id': 'https://github.com/dlongley', '@type': [ 'foaf:Person', 'earl:Assertor' ], 'foaf:name': 'Dave Longley', 'foaf:homepage': 'https://github.com/dlongley' }, 'doap:release': { 'doap:name': '', 'doap:revision': '', 'doap:created': today }, 'subjectOf': [] }; /* eslint-enable quote-props */ const version = require('../package.json').version; // FIXME: Improve "Node.js" vs "browser" id reporting // this._report['@id'] += '#' + this.id; // this._report['doap:name'] += ' ' + this.id; // this._report['dc:title'] += ' ' + this.id; this._report['doap:release']['doap:name'] = this._report['doap:name'] + ' ' + version; this._report['doap:release']['doap:revision'] = version; } EarlReport.prototype.addAssertion = function(test, pass) { this._report.subjectOf.push({ '@type': 'earl:Assertion', 'earl:assertedBy': this._report['doap:developer']['@id'], 'earl:mode': 'earl:automatic', 'earl:test': test['@id'], 'earl:result': { '@type': 'earl:TestResult', 'dc:date': this.now.toISOString(), 'earl:outcome': pass ? 'earl:passed' : 'earl:failed' } }); return this; }; EarlReport.prototype.report = function() { return this._report; }; EarlReport.prototype.reportJson = function() { return JSON.stringify(this._report, null, 2); }; module.exports = EarlReport; jsonld.js-4.0.1/tests/fromRdf-0001-in.nq000066400000000000000000000007151401135163200175020ustar00rootroot00000000000000 . # $5 comment . "Plain" . # Another comment "2012-05-12"^^ . "English"@en . jsonld.js-4.0.1/tests/fromRdf-0001-out.jsonld000066400000000000000000000005471401135163200205610ustar00rootroot00000000000000[ { "@id": "http://example.com/Subj1", "@type": ["http://example.com/Type"], "http://example.com/prop1": [{"@id": "http://example.com/Obj1"}], "http://example.com/prop2": [ {"@value": "Plain"}, {"@value": "2012-05-12", "@type": "http://www.w3.org/2001/XMLSchema#date"}, {"@value": "English", "@language": "en"} ] } ] jsonld.js-4.0.1/tests/graph-container.js000066400000000000000000000071411401135163200202000ustar00rootroot00000000000000/** * Temporary graph-container tests. */ // disable so tests can be copy & pasted /* eslint-disable quotes, quote-props */ const jsonld = require('..'); const assert = require('assert'); describe('@graph container', () => { it('should expand @graph container', done => { const doc = { "@context": { "@version": 1.1, "input": {"@id": "foo:input", "@container": "@graph"}, "value": "foo:value" }, "input": { "value": "x" } }; const p = jsonld.expand(doc); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(expanded => { assert.deepEqual(expanded, [{ "foo:input": [{ "@graph": [{ "foo:value": [{ "@value": "x" }] }] }] }]); done(); }); }); it('should expand ["@graph", "@set"] container', done => { const doc = { "@context": { "@version": 1.1, "input": {"@id": "foo:input", "@container": ["@graph", "@set"]}, "value": "foo:value" }, "input": [{ "value": "x" }] }; const p = jsonld.expand(doc); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(expanded => { assert.deepEqual(expanded, [{ "foo:input": [{ "@graph": [{ "foo:value": [{ "@value": "x" }] }] }] }]); done(); }); }); it('should expand and then compact @graph container', done => { const doc = { "@context": { "@version": 1.1, "input": {"@id": "foo:input", "@container": "@graph"}, "value": "foo:value" }, "input": { "value": "x" } }; const p = jsonld.expand(doc); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(expanded => { const p = jsonld.compact(expanded, doc['@context']); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(compacted => { assert.deepEqual(compacted, { "@context": { "@version": 1.1, "input": { "@id": "foo:input", "@container": "@graph" }, "value": "foo:value" }, "input": { "value": "x" } }); done(); }); }); }); it('should expand and then compact @graph container into a @set', done => { const doc = { "@context": { "@version": 1.1, "input": {"@id": "foo:input", "@container": "@graph"}, "value": "foo:value" }, "input": { "value": "x" } }; const newContext = { "@context": { "@version": 1.1, "input": {"@id": "foo:input", "@container": ["@graph", "@set"]}, "value": "foo:value" } }; const p = jsonld.expand(doc); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(expanded => { const p = jsonld.compact(expanded, newContext); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(compacted => { assert.deepEqual(compacted, { "@context": { "@version": 1.1, "input": { "@id": "foo:input", "@container": [ "@graph", "@set" ] }, "value": "foo:value" }, "input": [ { "value": "x" } ] }); done(); }); }); }); }); jsonld.js-4.0.1/tests/manifest.jsonld000066400000000000000000000036111401135163200176000ustar00rootroot00000000000000{ "@context": "http://json-ld.org/test-suite/context.jsonld", "@id": "", "@type": "mf:Manifest", "name": "Custom jsonld.js Tests", "description": "Custom jsonld.js Tests", "baseIri": "./", "sequence": [{ "@id": "#t0001", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve single URL", "input": "remote-0001-in.jsonld", "expect": "remote-0001-out.jsonld" }, { "@id": "#t0002", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve recursive URL", "input": "remote-0002-in.jsonld", "expect": "remote-0002-out.jsonld" }, { "@id": "#t0003", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve doubly recursive URL", "input": "remote-0003-in.jsonld", "expect": "remote-0003-out.jsonld" }, { "@id": "#t0004", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve array context", "input": "remote-0004-in.jsonld", "expect": "remote-0004-out.jsonld" }, { "@id": "#t0005", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve relative URL", "input": "remote-0005-in.jsonld", "expect": "remote-0005-out.jsonld" }, { "@id": "#t0006", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "follow redirect", "input": "remote-0006-in.jsonld", "expect": "remote-0006-out.jsonld" }, { "@id": "#t0007", "@type": ["jld:PositiveEvaluationTest", "jld:FromRDFTest"], "name": "allow comments in N-Quads", "purpose": "RDF serialized in N-Quads may contain comments", "input": "fromRdf-0001-in.nq", "expect": "fromRdf-0001-out.jsonld" }, { "@id": "#t0008", "@type": ["jld:PositiveEvaluationTest", "jld:ExpandTest"], "name": "resolve context null values", "input": "remote-0008-in.jsonld", "expect": "remote-0008-out.jsonld" }] } jsonld.js-4.0.1/tests/misc.js000066400000000000000000000253651401135163200160620ustar00rootroot00000000000000/** * Misc tests. */ // disable so tests can be copy & pasted /* eslint-disable quotes, quote-props */ const jsonld = require('..'); const assert = require('assert'); // TODO: need more tests for jsonld.link and jsonld.merge describe('link tests', () => { const doc = { "@id": "ex:1", "a:foo": { "@id": "ex:1" } }; it('should create a circular link', done => { const p = jsonld.link(doc, {}); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.equal(output, output['a:foo']); done(); }); }); }); describe('merge tests', () => { const docA = {"@id": "ex:1", "a:foo": [{"@value": 1}]}; const docB = {"@id": "ex:1", "b:foo": [{"@value": 2}]}; const merged = [Object.assign({}, docA, docB)]; const context = {}; const ctxMerged = {"@graph": [{"@id": "ex:1", "a:foo": 1, "b:foo": 2}]}; it('should merge nodes from two different documents', done => { const p = jsonld.merge([docA, docB]); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.deepEqual(output, merged); done(); }); }); it('should merge nodes from two different documents with context', done => { const p = jsonld.merge([docA, docB], context); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.deepEqual(output, ctxMerged); done(); }); }); }); describe('createNodeMap', () => { const doc = {"@id": "ex:1", "a:property": [{"@id": "ex:2"}]}; it('should create a flattened node hashmap', () => { const expected = { "ex:1": { "@id": "ex:1", "a:property": [ {"@id": "ex:2"} ] }, "ex:2": {"@id": "ex:2"} }; return jsonld.createNodeMap(doc).then(map => { assert.deepEqual(map, expected); }); }); }); describe('other toRDF tests', () => { const emptyRdf = []; it('should process with options and promise', done => { const p = jsonld.toRDF({}, {}); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.catch(e => { assert.fail(); }).then(output => { assert.deepEqual(output, emptyRdf); done(); }); }); it('should process with no options and promise', done => { const p = jsonld.toRDF({}); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.catch(e => { assert.fail(); }).then(output => { assert.deepEqual(output, emptyRdf); done(); }); }); it('should fail with no args and promise', done => { const p = jsonld.toRDF(); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.then(output => { assert.fail(); }).catch(e => { assert(e); done(); }); }); it('should fail for bad format and promise', done => { const p = jsonld.toRDF({}, {format: 'bogus'}); assert(p instanceof Promise); p.then(() => { assert.fail(); }).catch(e => { assert(e); assert.equal(e.name, 'jsonld.UnknownFormat'); done(); }); }); it('should handle N-Quads format', done => { const doc = { "@id": "https://example.com/", "https://example.com/test": "test" }; const p = jsonld.toRDF(doc, {format: 'application/n-quads'}); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.equal( output, ' "test" .\n'); done(); }); }); it('should handle deprecated N-Quads format', done => { const doc = { "@id": "https://example.com/", "https://example.com/test": "test" }; const p = jsonld.toRDF(doc, {format: 'application/nquads'}); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.equal( output, ' "test" .\n'); done(); }); }); }); describe('other fromRDF tests', () => { const emptyNQuads = ''; const emptyRdf = []; it('should process with options and promise', done => { const p = jsonld.fromRDF(emptyNQuads, {}); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.catch(e => { assert.fail(); }).then(output => { assert.deepEqual(output, emptyRdf); done(); }); }); it('should process with no options and promise', done => { const p = jsonld.fromRDF(emptyNQuads); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.catch(e => { assert.fail(); }).then(output => { assert.deepEqual(output, emptyRdf); done(); }); }); it('should fail with no args and promise', done => { const p = jsonld.fromRDF(); assert(p instanceof Promise); /* eslint-disable-next-line no-unused-vars */ p.then(output => { assert.fail(); }).catch(e => { assert(e); done(); }); }); it('should fail for bad format and promise', done => { const p = jsonld.fromRDF(emptyNQuads, {format: 'bogus'}); assert(p instanceof Promise); p.then(() => { assert.fail(); }).catch(e => { assert(e); assert.equal(e.name, 'jsonld.UnknownFormat'); done(); }); }); it('should handle N-Quads format', done => { const nq = ' "test" .\n'; const p = jsonld.fromRDF(nq, {format: 'application/n-quads'}); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.deepEqual( output, [{ "@id": "https://example.com/", "https://example.com/test": [{ "@value": "test" }] }]); done(); }); }); it('should handle deprecated N-Quads format', done => { const nq = ' "test" .\n'; const p = jsonld.fromRDF(nq, {format: 'application/nquads'}); assert(p instanceof Promise); p.catch(e => { assert.ifError(e); }).then(output => { assert.deepEqual( output, [{ "@id": "https://example.com/", "https://example.com/test": [{ "@value": "test" }] }]); done(); }); }); }); describe('loading multiple levels of contexts', () => { const documentLoader = url => { if(url === 'https://example.com/context1') { return { document: { "@context": { "ex": "https://example.com/#" } }, contextUrl: null, documentUrl: url }; } if(url === 'https://example.com/context2') { return { document: { "@context": { "ex": "https://example.com/#" } }, contextUrl: null, documentUrl: url }; } }; const doc = { "@context": "https://example.com/context1", "ex:foo": { "@context": "https://example.com/context2", "ex:bar": "test" } }; const expected = [{ "https://example.com/#foo": [{ "https://example.com/#bar": [{ "@value": "test" }] }] }]; it('should handle loading multiple levels of contexts (promise)', () => { return jsonld.expand(doc, {documentLoader}).then(output => { assert.deepEqual(output, expected); }); }); }); describe('url tests', () => { it('should detect absolute IRIs', done => { // absolute IRIs assert(jsonld.url.isAbsolute('a:')); assert(jsonld.url.isAbsolute('a:b')); assert(jsonld.url.isAbsolute('a:b:c')); // blank nodes assert(jsonld.url.isAbsolute('_:')); assert(jsonld.url.isAbsolute('_:a')); assert(jsonld.url.isAbsolute('_:a:b')); // not absolute or blank node assert(!jsonld.url.isAbsolute(':')); assert(!jsonld.url.isAbsolute('a')); assert(!jsonld.url.isAbsolute('/:')); assert(!jsonld.url.isAbsolute('/a:')); assert(!jsonld.url.isAbsolute('/a:b')); assert(!jsonld.url.isAbsolute('_')); done(); }); }); describe('js keywords', () => { it('expand js valueOf/toString keywords (top ctx)', async () => { const d = { "@context": { "valueOf": "http://example.org/valueOf", "toString": "http://example.org/toString" }, "valueOf": "first", "toString": "second" } ; const ex = [ { "http://example.org/toString": [ { "@value": "second" } ], "http://example.org/valueOf": [ { "@value": "first" } ] } ] ; const e = await jsonld.expand(d); assert.deepStrictEqual(e, ex); }); it('expand js valueOf/toString keywords (sub ctx)', async () => { const d = { "@context": { "@version": 1.1, "ex:thing": { "@context": { "valueOf": "http://example.org/valueOf", "toString": "http://example.org/toString" } } }, "ex:thing": { "valueOf": "first", "toString": "second" } } ; const ex = [ { "ex:thing": [ { "http://example.org/toString": [ { "@value": "second" } ], "http://example.org/valueOf": [ { "@value": "first" } ] } ] } ] ; const e = await jsonld.expand(d); assert.deepStrictEqual(e, ex); }); it('compact js valueOf/toString keywords', async () => { const d = { "@context": { "valueOf": "http://example.org/valueOf", "toString": "http://example.org/toString" }, "valueOf": "first", "toString": "second" } ; const ctx = { "@context": { "valueOf": "http://example.org/valueOf", "toString": "http://example.org/toString" } } ; const ex = { "@context": { "valueOf": "http://example.org/valueOf", "toString": "http://example.org/toString" }, "valueOf": "first", "toString": "second" } ; const e = await jsonld.compact(d, ctx); assert.deepStrictEqual(e, ex); }); it('frame js valueOf/toString keywords', async () => { const d = { "@context": { "@vocab": "http://example.org/" }, "toString": { "valueOf": "thing" } } ; const frame = { "@context": { "@vocab": "http://example.org/" }, "toString": {} } ; const ex = { "@context": { "@vocab": "http://example.org/" }, "toString": { "valueOf": "thing" } } ; const e = await jsonld.frame(d, frame); assert.deepStrictEqual(e, ex); }); }); describe('literal JSON', () => { it('handles error', done => { const d = '_:b0 "bogus"^^ .' ; const p = jsonld.fromRDF(d); assert(p instanceof Promise); p.then(() => { assert.fail(); }).catch(e => { assert(e); assert.equal(e.name, 'jsonld.InvalidJsonLiteral'); done(); }); }); }); jsonld.js-4.0.1/tests/node-document-loader-tests.js000066400000000000000000000070531401135163200222660ustar00rootroot00000000000000/** * Local tests for the Node.js document loader * * @author goofballLogic */ /* eslint-disable quote-props */ const jsonld = require('..'); const assert = require('assert'); describe('For the Node.js document loader', function() { const documentLoaderType = 'node'; const requestMock = function(options, callback) { // store these for later inspection requestMock.calls.push([].slice.call(arguments, 0)); callback(null, {headers: {}}, ''); }; describe('When built with no options specified', function() { it('loading should work', function(done) { jsonld.useDocumentLoader(documentLoaderType); /* eslint-disable-next-line no-unused-vars */ jsonld.expand('http://schema.org/', function(err, expanded) { assert.ifError(err); done(); }); }); }); describe('When built with no explicit headers', function() { const options = {request: requestMock}; it('loading should pass just the ld Accept header', function(done) { jsonld.useDocumentLoader(documentLoaderType, options); requestMock.calls = []; const iri = 'http://some.thing.test.com/my-thing.jsonld'; jsonld.documentLoader(iri, function(err) { if(err) { return done(err); } const actualOptions = (requestMock.calls[0] || {})[0] || {}; const actualHeaders = actualOptions.headers; const expectedHeaders = { 'Accept': 'application/ld+json, application/json' }; assert.deepEqual(actualHeaders, expectedHeaders); done(); }); }); }); describe('When built using options containing a headers object', function() { const options = {request: requestMock}; options.headers = { 'x-test-header-1': 'First value', 'x-test-two': 2.34, 'Via': '1.0 fred, 1.1 example.com (Apache/1.1)', 'Authorization': 'Bearer d783jkjaods9f87o83hj' }; /* eslint-disable indent */ it('loading should pass the headers through on the request', function(done) { jsonld.useDocumentLoader(documentLoaderType, options); requestMock.calls = []; const iri = 'http://some.thing.test.com/my-thing.jsonld'; jsonld.documentLoader(iri, function(err) { if(err) { return done(err); } const actualOptions = (requestMock.calls[0] || {})[0] || {}; const actualHeaders = actualOptions.headers; const expectedHeaders = { 'Accept': 'application/ld+json, application/json' }; for(const k in options.headers) { expectedHeaders[k] = options.headers[k]; } assert.deepEqual(actualHeaders, expectedHeaders); done(); }); }); /* eslint-enable indent */ }); /* eslint-disable indent */ describe('When built using headers that already contain an Accept header', function() { const options = {request: requestMock}; options.headers = { 'x-test-header-3': 'Third value', 'Accept': 'video/mp4' }; it('constructing the document loader should fail', function(done) { const expectedMessage = 'Accept header may not be specified as an option; ' + 'only "application/ld+json, application/json" is supported.'; assert.throws( jsonld.useDocumentLoader.bind(jsonld, documentLoaderType, options), function(err) { assert.ok( err instanceof RangeError, 'A range error should be thrown'); assert.equal(err.message, expectedMessage); return true; }); done(); }); }); /* eslint-enable indent */ }); jsonld.js-4.0.1/tests/remote-0001-in.jsonld000066400000000000000000000001151401135163200202430ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-1.jsonld", "term1": "foo" } jsonld.js-4.0.1/tests/remote-0001-out.jsonld000066400000000000000000000000771401135163200204530ustar00rootroot00000000000000[ { "http://example.org/term1": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0002-in.jsonld000066400000000000000000000001151401135163200202440ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-2.jsonld", "term1": "foo" } jsonld.js-4.0.1/tests/remote-0002-out.jsonld000066400000000000000000000000771401135163200204540ustar00rootroot00000000000000[ { "http://example.org/term1": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0003-in.jsonld000066400000000000000000000001151401135163200202450ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-3.jsonld", "term1": "foo" } jsonld.js-4.0.1/tests/remote-0003-out.jsonld000066400000000000000000000000771401135163200204550ustar00rootroot00000000000000[ { "http://example.org/term1": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0004-in.jsonld000066400000000000000000000001771401135163200202560ustar00rootroot00000000000000{ "@context": [ {"term1": "http://notexample.org"}, "http://localhost:8000/context-4.jsonld" ], "term1": "foo" } jsonld.js-4.0.1/tests/remote-0004-out.jsonld000066400000000000000000000000771401135163200204560ustar00rootroot00000000000000[ { "http://example.org/term1": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0005-in.jsonld000066400000000000000000000001151401135163200202470ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-5.jsonld", "term1": "foo" } jsonld.js-4.0.1/tests/remote-0005-out.jsonld000066400000000000000000000000771401135163200204570ustar00rootroot00000000000000[ { "http://example.org/term1": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0006-in.jsonld000066400000000000000000000001051401135163200202470ustar00rootroot00000000000000{ "@context": "https://w3id.org/payswarm/v1", "comment": "foo" } jsonld.js-4.0.1/tests/remote-0006-out.jsonld000066400000000000000000000001231401135163200204500ustar00rootroot00000000000000[ { "http://www.w3.org/2000/01/rdf-schema#comment": [{"@value": "foo"}] } ]jsonld.js-4.0.1/tests/remote-0008-in.jsonld000066400000000000000000000001501401135163200202510ustar00rootroot00000000000000{ "@context": "http://localhost:8000/context-0008.jsonld", "t1": "t1 should have been nulled out" } jsonld.js-4.0.1/tests/remote-0008-out.jsonld000066400000000000000000000000031401135163200204470ustar00rootroot00000000000000[] jsonld.js-4.0.1/tests/remote-context-server.js000066400000000000000000000005261401135163200214000ustar00rootroot00000000000000const cors = require('cors'); const express = require('express'); const path = require('path'); const app = express(); app.use(cors()); app.use(express.static(path.resolve(path.join(__dirname, 'contexts')))); const port = 8000; app.listen(port, function() { console.log('Remote context test server running on port ' + port + '...'); }); jsonld.js-4.0.1/tests/test-common.js000066400000000000000000000654771401135163200174040ustar00rootroot00000000000000/** * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. */ /* eslint-disable indent */ const EarlReport = require('./earl-report'); const benchmark = require('benchmark'); const join = require('join-path-js'); const rdfCanonize = require('rdf-canonize'); const {prependBase} = require('../lib/url'); module.exports = function(options) { 'use strict'; const assert = options.assert; const jsonld = options.jsonld; const manifest = options.manifest || { '@context': 'https://json-ld.org/test-suite/context.jsonld', '@id': '', '@type': 'mf:Manifest', description: 'Top level jsonld.js manifest', name: 'jsonld.js', sequence: options.entries || [], filename: '/' }; const TEST_TYPES = { 'jld:CompactTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ // html /html-manifest#tc001$/, /html-manifest#tc002$/, /html-manifest#tc003$/, /html-manifest#tc004$/, ] }, fn: 'compact', params: [ readTestUrl('input'), readTestJson('context'), createTestOptions() ], compare: compareExpectedJson }, 'jld:ExpandTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ // misc /expand-manifest#tc037$/, /expand-manifest#tc038$/, /expand-manifest#ter54$/, // html /html-manifest#te001$/, /html-manifest#te002$/, /html-manifest#te003$/, /html-manifest#te004$/, /html-manifest#te005$/, /html-manifest#te006$/, /html-manifest#te007$/, /html-manifest#te010$/, /html-manifest#te011$/, /html-manifest#te012$/, /html-manifest#te013$/, /html-manifest#te014$/, /html-manifest#te015$/, /html-manifest#te016$/, /html-manifest#te017$/, /html-manifest#te018$/, /html-manifest#te019$/, /html-manifest#te020$/, /html-manifest#te021$/, /html-manifest#te022$/, /html-manifest#tex01$/, // HTML extraction /expand-manifest#thc01$/, /expand-manifest#thc02$/, /expand-manifest#thc03$/, /expand-manifest#thc04$/, /expand-manifest#thc05$/, // remote /remote-doc-manifest#t0013$/, // HTML ] }, fn: 'expand', params: [ readTestUrl('input'), createTestOptions() ], compare: compareExpectedJson }, 'jld:FlattenTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ // html /html-manifest#tf001$/, /html-manifest#tf002$/, /html-manifest#tf003$/, /html-manifest#tf004$/, ] }, fn: 'flatten', params: [ readTestUrl('input'), readTestJson('context'), createTestOptions() ], compare: compareExpectedJson }, 'jld:FrameTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ ] }, fn: 'frame', params: [ readTestUrl('input'), readTestJson('frame'), createTestOptions() ], compare: compareExpectedJson }, 'jld:FromRDFTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ // direction (compound-literal) /fromRdf-manifest#tdi11$/, /fromRdf-manifest#tdi12$/, ] }, fn: 'fromRDF', params: [ readTestNQuads('input'), createTestOptions({format: 'application/n-quads'}) ], compare: compareExpectedJson }, 'jld:NormalizeTest': { fn: 'normalize', params: [ readTestUrl('input'), createTestOptions({format: 'application/n-quads'}) ], compare: compareExpectedNQuads }, 'jld:ToRDFTest': { skip: { // skip tests where behavior changed for a 1.1 processor // see JSON-LD 1.0 Errata specVersion: ['json-ld-1.0'], // FIXME // NOTE: idRegex format: //MMM-manifest#tNNN$/, idRegex: [ // misc /toRdf-manifest#tc037$/, /toRdf-manifest#tc038$/, /toRdf-manifest#ter54$/, /toRdf-manifest#tli12$/, /toRdf-manifest#tli14$/, // well formed /toRdf-manifest#twf05$/, // html /html-manifest#tr001$/, /html-manifest#tr002$/, /html-manifest#tr003$/, /html-manifest#tr004$/, /html-manifest#tr005$/, /html-manifest#tr006$/, /html-manifest#tr007$/, /html-manifest#tr010$/, /html-manifest#tr011$/, /html-manifest#tr012$/, /html-manifest#tr013$/, /html-manifest#tr014$/, /html-manifest#tr015$/, /html-manifest#tr016$/, /html-manifest#tr017$/, /html-manifest#tr018$/, /html-manifest#tr019$/, /html-manifest#tr020$/, /html-manifest#tr021$/, /html-manifest#tr022$/, // Invalid Statement /toRdf-manifest#te075$/, /toRdf-manifest#te111$/, /toRdf-manifest#te112$/, // direction (compound-literal) /toRdf-manifest#tdi11$/, /toRdf-manifest#tdi12$/, ] }, fn: 'toRDF', params: [ readTestUrl('input'), createTestOptions({format: 'application/n-quads'}) ], compare: compareCanonizedExpectedNQuads }, 'rdfn:Urgna2012EvalTest': { fn: 'normalize', params: [ readTestNQuads('action'), createTestOptions({ algorithm: 'URGNA2012', inputFormat: 'application/n-quads', format: 'application/n-quads' }) ], compare: compareExpectedNQuads }, 'rdfn:Urdna2015EvalTest': { fn: 'normalize', params: [ readTestNQuads('action'), createTestOptions({ algorithm: 'URDNA2015', inputFormat: 'application/n-quads', format: 'application/n-quads' }) ], compare: compareExpectedNQuads } }; const SKIP_TESTS = []; // create earl report if(options.earl && options.earl.filename) { options.earl.report = new EarlReport({id: options.earl.id}); } return new Promise(resolve => { // async generated tests // _tests => [{suite}, ...] // suite => { // title: ..., // tests: [test, ...], // suites: [suite, ...] // } const _tests = []; return addManifest(manifest, _tests) .then(() => { _testsToMocha(_tests); }).then(() => { if(options.earl.report) { describe('Writing EARL report to: ' + options.earl.filename, function() { it('should print the earl report', function() { return options.writeFile( options.earl.filename, options.earl.report.reportJson()); }); }); } }).then(() => resolve()); // build mocha tests from local test structure function _testsToMocha(tests) { tests.forEach(suite => { if(suite.skip) { describe.skip(suite.title); return; } describe(suite.title, () => { suite.tests.forEach(test => { if(test.only) { it.only(test.title, test.f); return; } it(test.title, test.f); }); _testsToMocha(suite.suites); }); suite.imports.forEach(f => { options.import(f); }); }); } }); /** * Adds the tests for all entries in the given manifest. * * @param manifest {Object} the manifest. * @param parent {Object} the parent test structure * @return {Promise} */ function addManifest(manifest, parent) { return new Promise((resolve, reject) => { // create test structure const suite = { title: manifest.name || manifest.label, tests: [], suites: [], imports: [] }; parent.push(suite); // get entries and sequence (alias for entries) const entries = [].concat( getJsonLdValues(manifest, 'entries'), getJsonLdValues(manifest, 'sequence') ); const includes = getJsonLdValues(manifest, 'include'); // add includes to sequence as jsonld files for(let i = 0; i < includes.length; ++i) { entries.push(includes[i] + '.jsonld'); } // resolve all entry promises and process Promise.all(entries).then(entries => { let p = Promise.resolve(); entries.forEach(entry => { if(typeof entry === 'string' && entry.endsWith('js')) { // process later as a plain JavaScript file suite.imports.push(entry); return; } else if(typeof entry === 'function') { // process as a function that returns a promise p = p.then(() => { return entry(options); }).then(childSuite => { if(suite) { suite.suites.push(childSuite); } }); return; } p = p.then(() => { return readManifestEntry(manifest, entry); }).then(entry => { if(isJsonLdType(entry, '__SKIP__')) { // special local skip logic suite.tests.push(entry); } else if(isJsonLdType(entry, 'mf:Manifest')) { // entry is another manifest return addManifest(entry, suite.suites); } else { // assume entry is a test return addTest(manifest, entry, suite.tests); } }); }); return p; }).then(() => { resolve(); }).catch(err => { console.error(err); reject(err); }); }); } /** * Adds a test. * * @param manifest {Object} the manifest. * @param parent {Object} the test. * @param tests {Array} the list of tests to add to. * @return {Promise} */ function addTest(manifest, test, tests) { // expand @id and input base const test_id = test['@id'] || test['id']; //var number = test_id.substr(2); test['@id'] = manifest.baseIri + basename(manifest.filename).replace('.jsonld', '') + test_id; test.base = manifest.baseIri + test.input; test.manifest = manifest; const description = test_id + ' ' + (test.purpose || test.name); const _test = { title: description, f: makeFn() }; // only based on test manifest // skip handled via skip() if('only' in test) { _test.only = test.only; } tests.push(_test); function makeFn() { return async function() { const self = this; self.timeout(5000); const testInfo = TEST_TYPES[getJsonLdTestType(test)]; // skip based on test manifest if('skip' in test && test.skip) { if(options.verboseSkip) { console.log('Skipping test due to manifest:', {id: test['@id'], name: test.name}); } self.skip(); } // skip based on unknown test type const testTypes = Object.keys(TEST_TYPES); if(!isJsonLdType(test, testTypes)) { if(options.verboseSkip) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') ); console.log('Skipping test due to unknown type:', {id: test['@id'], name: test.name, type}); } self.skip(); } // skip based on test type if(isJsonLdType(test, SKIP_TESTS)) { if(options.verboseSkip) { const type = [].concat( getJsonLdValues(test, '@type'), getJsonLdValues(test, 'type') ); console.log('Skipping test due to test type:', {id: test['@id'], name: test.name, type}); } self.skip(); } // skip based on type info if(testInfo.skip && testInfo.skip.type) { if(options.verboseSkip) { console.log('Skipping test due to type info:', {id: test['@id'], name: test.name}); } self.skip(); } // skip based on id regex if(testInfo.skip && testInfo.skip.idRegex) { testInfo.skip.idRegex.forEach(function(re) { if(re.test(test['@id'])) { if(options.verboseSkip) { console.log('Skipping test due to id:', {id: test['@id']}); } self.skip(); } }); } // skip based on description regex if(testInfo.skip && testInfo.skip.descriptionRegex) { testInfo.skip.descriptionRegex.forEach(function(re) { if(re.test(description)) { if(options.verboseSkip) { console.log('Skipping test due to description:', {id: test['@id'], name: test.name, description}); } self.skip(); } }); } // Make expandContext absolute to the manifest if(test.hasOwnProperty('option') && test.option.expandContext) { test.option.expandContext = prependBase(test.manifest.baseIri, test.option.expandContext); } const testOptions = getJsonLdValues(test, 'option'); testOptions.forEach(function(opt) { const processingModes = getJsonLdValues(opt, 'processingMode'); processingModes.forEach(function(pm) { let skipModes = []; if(testInfo.skip && testInfo.skip.processingMode) { skipModes = testInfo.skip.processingMode; } if(skipModes.indexOf(pm) !== -1) { if(options.verboseSkip) { console.log('Skipping test due to processingMode:', {id: test['@id'], name: test.name, processingMode: pm}); } self.skip(); } }); }); testOptions.forEach(function(opt) { const specVersions = getJsonLdValues(opt, 'specVersion'); specVersions.forEach(function(sv) { let skipVersions = []; if(testInfo.skip && testInfo.skip.specVersion) { skipVersions = testInfo.skip.specVersion; } if(skipVersions.indexOf(sv) !== -1) { if(options.verboseSkip) { console.log('Skipping test due to specVersion:', {id: test['@id'], name: test.name, specVersion: sv}); } self.skip(); } }); }); const fn = testInfo.fn; const params = testInfo.params.map(param => param(test)); // resolve test data const values = await Promise.all(params); let err; let result; // run and capture errors and results try { result = await jsonld[fn].apply(null, values); } catch(e) { err = e; } try { if(isJsonLdType(test, 'jld:NegativeEvaluationTest')) { await compareExpectedError(test, err); } else if(isJsonLdType(test, 'jld:PositiveEvaluationTest') || isJsonLdType(test, 'rdfn:Urgna2012EvalTest') || isJsonLdType(test, 'rdfn:Urdna2015EvalTest')) { if(err) { throw err; } await testInfo.compare(test, result); } else if(isJsonLdType(test, 'jld:PositiveSyntaxTest')) { // no checks } else { throw Error('Unknown test type: ' + test.type); } if(options.benchmark) { // pre-load params to avoid doc loader and parser timing const benchParams = testInfo.params.map(param => param(test, { load: true })); const benchValues = await Promise.all(benchParams); await new Promise((resolve, reject) => { const suite = new benchmark.Suite(); suite.add({ name: test.name, defer: true, fn: deferred => { jsonld[fn].apply(null, benchValues).then(() => { deferred.resolve(); }); } }); suite .on('start', e => { self.timeout((e.target.maxTime + 2) * 1000); }) .on('cycle', e => { console.log(String(e.target)); }) .on('error', err => { reject(new Error(err)); }) .on('complete', () => { resolve(); }) .run({async: true}); }); } if(options.earl.report) { options.earl.report.addAssertion(test, true); } } catch(err) { if(options.bailOnError) { if(err.name !== 'AssertionError') { console.error('\nError: ', JSON.stringify(err, null, 2)); } options.exit(); } if(options.earl.report) { options.earl.report.addAssertion(test, false); } console.error('Error: ', JSON.stringify(err, null, 2)); throw err; } }; } } function getJsonLdTestType(test) { const types = Object.keys(TEST_TYPES); for(let i = 0; i < types.length; ++i) { if(isJsonLdType(test, types[i])) { return types[i]; } } return null; } function readManifestEntry(manifest, entry) { let p = Promise.resolve(); let _entry = entry; if(typeof entry === 'string') { let _filename; p = p.then(() => { if(entry.endsWith('json') || entry.endsWith('jsonld')) { // load as file return entry; } // load as dir with manifest.jsonld return joinPath(entry, 'manifest.jsonld'); }).then(entry => { const dir = dirname(manifest.filename); return joinPath(dir, entry); }).then(filename => { _filename = filename; return readJson(filename); }).then(entry => { _entry = entry; _entry.filename = _filename; return _entry; }).catch(err => { if(err.code === 'ENOENT') { //console.log('File does not exist, skipping: ' + _filename); // return a "skip" entry _entry = { type: '__SKIP__', title: 'Not found, skipping: ' + _filename, filename: _filename, skip: true }; return; } throw err; }); } return p.then(() => { _entry.dirname = dirname(_entry.filename || manifest.filename); return _entry; }); } function readTestUrl(property) { return async function(test, options) { if(!test[property]) { return null; } if(options && options.load) { // always load const filename = await joinPath(test.dirname, test[property]); return readJson(filename); } return test.manifest.baseIri + test[property]; }; } function readTestJson(property) { return async function(test) { if(!test[property]) { return null; } const filename = await joinPath(test.dirname, test[property]); return readJson(filename); }; } function readTestNQuads(property) { return async function(test) { if(!test[property]) { return null; } const filename = await joinPath(test.dirname, test[property]); return readFile(filename); }; } function createTestOptions(opts) { return function(test) { const options = { documentLoader: createDocumentLoader(test) }; const httpOptions = ['contentType', 'httpLink', 'httpStatus', 'redirectTo']; const testOptions = test.option || {}; for(const key in testOptions) { if(httpOptions.indexOf(key) === -1) { options[key] = testOptions[key]; } } if(opts) { // extend options for(const key in opts) { options[key] = opts[key]; } } return options; }; } // find the expected output property or throw error function _getExpectProperty(test) { if('expectErrorCode' in test) { return 'expectErrorCode'; } else if('expect' in test) { return 'expect'; } else if('result' in test) { return 'result'; } else { throw Error('No expected output property found'); } } async function compareExpectedJson(test, result) { let expect; try { expect = await readTestJson(_getExpectProperty(test))(test); assert.deepStrictEqual(result, expect); } catch(err) { if(options.bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED: ' + JSON.stringify(expect, null, 2)); console.log('ACTUAL: ' + JSON.stringify(result, null, 2)); } throw err; } } async function compareExpectedNQuads(test, result) { let expect; try { expect = await readTestNQuads(_getExpectProperty(test))(test); assert.strictEqual(result, expect); } catch(ex) { if(options.bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED:\n' + expect); console.log('ACTUAL:\n' + result); } throw ex; } } async function compareCanonizedExpectedNQuads(test, result) { let expect; try { expect = await readTestNQuads(_getExpectProperty(test))(test); const opts = {algorithm: 'URDNA2015'}; const expectDataset = rdfCanonize.NQuads.parse(expect); const expectCmp = await rdfCanonize.canonize(expectDataset, opts); const resultDataset = rdfCanonize.NQuads.parse(result); const resultCmp = await rdfCanonize.canonize(resultDataset, opts); assert.strictEqual(resultCmp, expectCmp); } catch(err) { if(options.bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED:\n' + expect); console.log('ACTUAL:\n' + result); } throw err; } } async function compareExpectedError(test, err) { let expect; let result; try { expect = test[_getExpectProperty(test)]; result = getJsonLdErrorCode(err); assert.ok(err, 'no error present'); assert.strictEqual(result, expect); } catch(_err) { if(options.bailOnError) { console.log('\nTEST FAILED\n'); console.log('EXPECTED: ' + expect); console.log('ACTUAL: ' + result); } // log the unexpected error to help with debugging console.log('Unexpected error:', err); throw _err; } } function isJsonLdType(node, type) { const nodeType = [].concat( getJsonLdValues(node, '@type'), getJsonLdValues(node, 'type') ); type = Array.isArray(type) ? type : [type]; for(let i = 0; i < type.length; ++i) { if(nodeType.indexOf(type[i]) !== -1) { return true; } } return false; } function getJsonLdValues(node, property) { let rval = []; if(property in node) { rval = node[property]; if(!Array.isArray(rval)) { rval = [rval]; } } return rval; } function getJsonLdErrorCode(err) { if(!err) { return null; } if(err.details) { if(err.details.code) { return err.details.code; } if(err.details.cause) { return getJsonLdErrorCode(err.details.cause); } } return err.name; } async function readJson(filename) { const data = await readFile(filename); return JSON.parse(data); } async function readFile(filename) { return options.readFile(filename); } async function joinPath() { return join.apply(null, Array.prototype.slice.call(arguments)); } function dirname(filename) { if(options.nodejs) { return options.nodejs.path.dirname(filename); } const idx = filename.lastIndexOf('/'); if(idx === -1) { return filename; } return filename.substr(0, idx); } function basename(filename) { if(options.nodejs) { return options.nodejs.path.basename(filename); } const idx = filename.lastIndexOf('/'); if(idx === -1) { return filename; } return filename.substr(idx + 1); } /** * Creates a test remote document loader. * * @param test the test to use the document loader for. * * @return the document loader. */ function createDocumentLoader(test) { const localBases = [ 'http://json-ld.org/test-suite', 'https://json-ld.org/test-suite', 'https://w3c.github.io/json-ld-api/tests', 'https://w3c.github.io/json-ld-framing/tests' ]; const localLoader = function(url) { // always load remote-doc tests remotely in node // NOTE: disabled due to github pages issues. //if(options.nodejs && test.manifest.name === 'Remote document') { // return jsonld.documentLoader(url); //} // FIXME: this check only works for main test suite and will not work if: // - running other tests and main test suite not installed // - use other absolute URIs but want to load local files const isTestSuite = localBases.some(function(base) { return url.startsWith(base); }); // TODO: improve this check const isRelative = url.indexOf(':') === -1; if(isTestSuite || isRelative) { // attempt to load official test-suite files or relative URLs locally return loadLocally(url); } // load remotely return jsonld.documentLoader(url); }; return localLoader; function loadLocally(url) { const doc = {contextUrl: null, documentUrl: url, document: null}; const options = test.option; if(options && url === test.base) { if('redirectTo' in options && parseInt(options.httpStatus, 10) >= 300) { doc.documentUrl = test.manifest.baseIri + options.redirectTo; } else if('httpLink' in options) { let contentType = options.contentType || null; if(!contentType && url.indexOf('.jsonld', url.length - 7) !== -1) { contentType = 'application/ld+json'; } if(!contentType && url.indexOf('.json', url.length - 5) !== -1) { contentType = 'application/json'; } let linkHeader = options.httpLink; if(Array.isArray(linkHeader)) { linkHeader = linkHeader.join(','); } const linkHeaders = jsonld.parseLinkHeader(linkHeader); const linkedContext = linkHeaders['http://www.w3.org/ns/json-ld#context']; if(linkedContext && contentType !== 'application/ld+json') { if(Array.isArray(linkedContext)) { throw {name: 'multiple context link headers'}; } doc.contextUrl = linkedContext.target; } // If not JSON-LD, alternate may point there if(linkHeaders['alternate'] && linkHeaders['alternate'].type == 'application/ld+json' && !(contentType || '').match(/^application\/(\w*\+)?json$/)) { doc.documentUrl = prependBase(url, linkHeaders['alternate'].target); } } } let p = Promise.resolve(); if(doc.documentUrl.indexOf(':') === -1) { p = p.then(() => { return joinPath(test.manifest.dirname, doc.documentUrl); }).then(filename => { doc.documentUrl = 'file://' + filename; return filename; }); } else { p = p.then(() => { return joinPath( test.manifest.dirname, doc.documentUrl.substr(test.manifest.baseIri.length)); }).then(fn => { return fn; }); } return p.then(readJson).then(json => { doc.document = json; return doc; }).catch(() => { throw {name: 'loading document failed', url}; }); } } }; jsonld.js-4.0.1/tests/test-karma.js000066400000000000000000000075411401135163200171730ustar00rootroot00000000000000/** * Karma test runner for jsonld.js. * * Use environment vars to control, set via karma.conf.js/webpack: * * Set dirs, manifests, or js to run: * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename * Bail with tests fail: * BAIL=true * Verbose skip reasons: * VERBOSE_SKIP=true * Benchmark mode: * Basic: * JSONLD_BENCHMARK=1 * With options: * JSONLD_BENCHMARK=key1=value1,key2=value2,... * * @author Dave Longley * @author David I. Lehn * * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. */ /* global serverRequire */ // FIXME: hack to ensure delay is set first mocha.setup({delay: true, ui: 'bdd'}); // test suite compatibility require('core-js/features/string/ends-with'); require('core-js/features/string/starts-with'); // jsonld compatibility require('core-js/features/array/from'); require('core-js/features/array/includes'); require('core-js/features/map'); require('core-js/features/object/assign'); require('core-js/features/object/entries'); require('core-js/features/promise'); require('core-js/features/set'); require('core-js/features/symbol'); const assert = require('chai').assert; const common = require('./test-common'); const jsonld = require('..'); const server = require('karma-server-side'); const webidl = require('./test-webidl'); const join = require('join-path-js'); const entries = []; if(process.env.JSONLD_TESTS) { entries.push(...process.env.JSONLD_TESTS.split(' ')); } else { const _top = process.env.TEST_ROOT_DIR; // TODO: support just adding certain entries in EARL mode? // json-ld-api main test suite // FIXME: add path detection entries.push(join(_top, 'test-suites/json-ld-api/tests')); entries.push(join(_top, '../json-ld-api/tests')); // json-ld-framing main test suite // FIXME: add path detection entries.push(join(_top, 'test-suites/json-ld-framing/tests')); entries.push(join(_top, '../json-ld-framing/tests')); /* // TODO: use json-ld-framing once tests are moved // json-ld.org framing test suite // FIXME: add path detection entries.push(join( _top, 'test-suites/json-ld.org/test-suite/tests/frame-manifest.jsonld')); entries.push(join( _top, '../json-ld.org/test-suite/tests/frame-manifests.jsonld')); */ // json-ld.org normalization test suite // FIXME: add path detection entries.push(join(_top, 'test-suites/normalization/tests')); entries.push(join(_top, '../normalization/tests')); // other tests entries.push(join(_top, 'tests/new-embed-api')); // WebIDL tests entries.push(webidl); } let benchmark = null; if(process.env.JSONLD_BENCHMARK) { benchmark = {}; if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { const kv = pair.split('='); benchmark[kv[0]] = kv[1]; }); } } const options = { nodejs: false, assert, jsonld, /* eslint-disable-next-line no-unused-vars */ exit: code => { console.error('exit not implemented'); throw new Error('exit not implemented'); }, earl: { id: 'browser', filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, benchmark, readFile: filename => { return server.run(filename, function(filename) { const fs = serverRequire('fs-extra'); return fs.readFile(filename, 'utf8').then(data => { return data; }); }); }, writeFile: (filename, data) => { return server.run(filename, data, function(filename, data) { const fs = serverRequire('fs-extra'); return fs.outputFile(filename, data); }); }, /* eslint-disable-next-line no-unused-vars */ import: f => { console.error('import not implemented'); } }; // wait for setup of all tests then run mocha common(options).then(() => { run(); }).catch(err => { console.error(err); }); jsonld.js-4.0.1/tests/test-webidl.js000066400000000000000000000061311401135163200173400ustar00rootroot00000000000000/** * Web IDL test runner for JSON-LD. * * @author Dave Longley * * Copyright (c) 2011-2017 Digital Bazaar, Inc. All rights reserved. */ /* global IdlArray */ /* global add_result_callback */ /* global add_completion_callback */ /* global done */ /* eslint-disable no-var */ const assert = require('chai').assert; require('./webidl/testharness.js'); require('./webidl/WebIDLParser.js'); require('./webidl/idlharness.js'); /* eslint-disable indent */ module.exports = options => { 'use strict'; return new Promise((resolve, reject) => { // add mocha suite const suite = { title: 'WebIDL', tests: [], suites: [], imports: [] }; //add_start_callback(() => {}); //add_test_state_callback((test) => {}); add_result_callback(function(test) { var _test = { title: test.name, f: null }; suite.tests.push(_test); _test.f = function(done) { var msg = test.message || ''; /* // HACK: PhantomJS can't set prototype to non-writable? if(msg.indexOf( 'JsonLdProcessor.prototype is writable expected false') !== -1) { test.status = 0; } // HACK: PhantomJS can't set window property to non-enumerable? if(msg.indexOf( '"JsonLdProcessor" is enumerable expected false') !== -1) { test.status = 0; } */ /* // HACK: PhantomJS issues if(msg.indexOf( 'JsonLdProcessor.length should be configurable expected true') !== -1) { this.skip(); } if(msg.indexOf( 'JsonLdProcessor.name should be configurable expected true') !== -1) { this.skip(); } */ // HACK: Aggressive optimization of class name? if(msg.indexOf( 'wrong value for JsonLdProcessor.name ' + 'expected "JsonLdProcessor" but got "t"') !== -1) { this.skip(); } if(msg.indexOf( 'wrong value for JsonLdProcessor.name ' + 'expected "JsonLdProcessor" but got "e"') !== -1) { this.skip(); } //earl.addAssertion({'@id': ?}, test.status === 0); assert.equal(test.status, 0, test.message); done(); }; }); /* eslint-disable-next-line no-unused-vars */ add_completion_callback(function(tests, status) { resolve(suite); }); // FIXME: should this be in main lib? is there a better way? // ensure that stringification tests are passed var toString = Object.prototype.toString; Object.prototype.toString = function() { // FIXME: is proto output needed? if(this === window.JsonLdProcessor.prototype) { return '[object JsonLdProcessorPrototype]'; } else if(this && this.constructor === window.JsonLdProcessor) { return '[object JsonLdProcessor]'; } return toString.apply(this, arguments); }; options.readFile('./tests/webidl/JsonLdProcessor.idl').then(idl => { setup({explicit_done: true}); var idl_array = new IdlArray(); idl_array.add_idls(idl); idl_array.add_objects({JsonLdProcessor: ['new JsonLdProcessor()']}); idl_array.test(); done(); }).catch(err => { console.error('WebIDL Error', err); reject(err); }); }); }; jsonld.js-4.0.1/tests/test.js000066400000000000000000000071061401135163200160770ustar00rootroot00000000000000/** * Node.js test runner for jsonld.js. * * Use environment vars to control: * * Set dirs, manifests, or js to run: * JSONLD_TESTS="r1 r2 ..." * Output an EARL report: * EARL=filename * Bail with tests fail: * BAIL=true * Verbose skip reasons: * VERBOSE_SKIP=true * Benchmark mode: * Basic: * JSONLD_BENCHMARK=1 * With options: * JSONLD_BENCHMARK=key1=value1,key2=value2,... * * @author Dave Longley * @author David I. Lehn * * Copyright (c) 2011-2019 Digital Bazaar, Inc. All rights reserved. */ // support async/await tests in node6 if(!require('semver').gte(process.version, '8.6.0')) { require('@babel/register')({ presets: [ [ '@babel/preset-env', { targets: 'node 6' } ] ] }); } const assert = require('chai').assert; const common = require('./test-common'); const fs = require('fs-extra'); const jsonld = require('..'); const path = require('path'); const entries = []; if(process.env.JSONLD_TESTS) { entries.push(...process.env.JSONLD_TESTS.split(' ')); } else { const _top = path.resolve(__dirname, '..'); // json-ld-api main test suite const apiPath = path.resolve(_top, 'test-suites/json-ld-api/tests'); if(fs.existsSync(apiPath)) { entries.push(apiPath); } else { // default to sibling dir entries.push(path.resolve(_top, '../json-ld-api/tests')); } // json-ld-framing main test suite const framingPath = path.resolve(_top, 'test-suites/json-ld-framing/tests'); if(fs.existsSync(framingPath)) { entries.push(framingPath); } else { // default to sibling dir entries.push(path.resolve(_top, '../json-ld-framing/tests')); } /* // TODO: use json-ld-framing once tests are moved // json-ld.org framing test suite const framingPath = path.resolve( _top, 'test-suites/json-ld.org/test-suite/tests/frame-manifest.jsonld'); if(fs.existsSync(framingPath)) { entries.push(framingPath); } else { // default to sibling dir entries.push(path.resolve( _top, '../json-ld.org/test-suite/tests/frame-manifest.jsonld')); } */ // json-ld.org normalization test suite const normPath = path.resolve(_top, 'test-suites/normalization/tests'); if(fs.existsSync(normPath)) { entries.push(normPath); } else { // default up to sibling dir entries.push(path.resolve(_top, '../normalization/tests')); } // other tests entries.push(path.resolve(_top, 'tests/misc.js')); entries.push(path.resolve(_top, 'tests/graph-container.js')); // TODO: avoid network traffic and re-enable //entries.push(path.resolve(_top, 'tests/node-document-loader-tests.js')); } let benchmark = null; if(process.env.JSONLD_BENCHMARK) { benchmark = {}; if(!(['1', 'true'].includes(process.env.JSONLD_BENCHMARK))) { process.env.JSONLD_BENCHMARK.split(',').forEach(pair => { const kv = pair.split('='); benchmark[kv[0]] = kv[1]; }); } } const options = { nodejs: { path }, assert, jsonld, exit: code => process.exit(code), earl: { id: 'Node.js', filename: process.env.EARL }, verboseSkip: process.env.VERBOSE_SKIP === 'true', bailOnError: process.env.BAIL === 'true', entries, benchmark, readFile: filename => { return fs.readFile(filename, 'utf8'); }, writeFile: (filename, data) => { return fs.outputFile(filename, data); }, import: f => require(f) }; // wait for setup of all tests then run mocha common(options).then(() => { run(); }).catch(err => { console.error(err); }); process.on('unhandledRejection', (reason, p) => { console.error('Unhandled Rejection at:', p, 'reason:', reason); }); jsonld.js-4.0.1/tests/webidl/000077500000000000000000000000001401135163200160245ustar00rootroot00000000000000jsonld.js-4.0.1/tests/webidl/JsonLdProcessor.idl000066400000000000000000000045151401135163200216140ustar00rootroot00000000000000[Constructor] interface JsonLdProcessor { static Promise compact(JsonLdInput input, JsonLdContext context, optional JsonLdOptions? options); static Promise> expand(JsonLdInput input, optional JsonLdOptions? options); static Promise flatten(JsonLdInput input, optional JsonLdContext? context, optional JsonLdOptions? options); }; dictionary JsonLdDictionary { }; typedef (JsonLdDictionary or sequence or USVString) JsonLdInput; typedef (JsonLdDictionary or USVString or sequence<(JsonLdDictionary or USVString)>) JsonLdContext; dictionary JsonLdOptions { USVString? base; boolean compactArrays = true; LoadDocumentCallback? documentLoader = null; (JsonLdDictionary? or USVString) expandContext = null; boolean produceGeneralizedRdf = true; USVString? processingMode = null; boolean compactToRelative = true; }; callback LoadDocumentCallback = Promise (USVString url); dictionary RemoteDocument { USVString contextUrl = null; USVString documentUrl; any document; }; dictionary JsonLdError { JsonLdErrorCode code; USVString? message = null; }; enum JsonLdErrorCode { "colliding keywords", "conflicting indexes", "cyclic IRI mapping", "invalid @id value", "invalid @index value", "invalid @nest value", "invalid @prefix value", "invalid @reverse value", "invalid @version value", "invalid base IRI", "invalid container mapping", "invalid default language", "invalid IRI mapping", "invalid keyword alias", "invalid language map value", "invalid language mapping", "invalid language-tagged string", "invalid language-tagged value", "invalid local context", "invalid remote context", "invalid reverse property", "invalid reverse property map", "invalid reverse property value", "invalid scoped context", "invalid set or list object", "invalid term definition", "invalid type mapping", "invalid type value", "invalid typed value", "invalid value object", "invalid value object value", "invalid vocab mapping", "keyword redefinition", "loading document failed", "loading remote context failed", "multiple context link headers", "processing mode conflict", "recursive context inclusion" }; jsonld.js-4.0.1/tests/webidl/WebIDLParser.js000066400000000000000000001007621401135163200206130ustar00rootroot00000000000000(function() { var tokenise = function(str) { var tokens = [], re = { "float": /^-?(([0-9]+\.[0-9]*|[0-9]*\.[0-9]+)([Ee][-+]?[0-9]+)?|[0-9]+[Ee][-+]?[0-9]+)/, "integer": /^-?(0([Xx][0-9A-Fa-f]+|[0-7]*)|[1-9][0-9]*)/, "identifier": /^[A-Z_a-z][0-9A-Z_a-z-]*/, "string": /^"[^"]*"/, "whitespace": /^(?:[\t\n\r ]+|[\t\n\r ]*((\/\/.*|\/\*(.|\n|\r)*?\*\/)[\t\n\r ]*))+/, "other": /^[^\t\n\r 0-9A-Z_a-z]/ }, types = ["float", "integer", "identifier", "string", "whitespace", "other"]; while (str.length > 0) { var matched = false; for (var i = 0, n = types.length; i < n; i++) { var type = types[i]; str = str.replace(re[type], function(tok) { tokens.push({ type: type, value: tok }); matched = true; return ""; }); if (matched) break; } if (matched) continue; throw new Error("Token stream not progressing"); } return tokens; }; function WebIDLParseError(str, line, input, tokens) { this.message = str; this.line = line; this.input = input; this.tokens = tokens; }; WebIDLParseError.prototype.toString = function() { return this.message + ", line " + this.line + " (tokens: '" + this.input + "')\n" + JSON.stringify(this.tokens, null, 4); }; var parse = function(tokens, opt) { var line = 1; tokens = tokens.slice(); var FLOAT = "float", INT = "integer", ID = "identifier", STR = "string", OTHER = "other"; var error = function(str) { var tok = ""; var numTokens = 0; var maxTokens = 5; while (numTokens < maxTokens && tokens.length > numTokens) { tok += tokens[numTokens].value; numTokens++; } throw new WebIDLParseError(str, line, tok, tokens.slice(0, 5)); }; var last_token = null; var consume = function(type, value) { if (!tokens.length || tokens[0].type !== type) return; if (typeof value === "undefined" || tokens[0].value === value) { last_token = tokens.shift(); if (type === ID) last_token.value = last_token.value.replace(/^_/, ""); return last_token; } }; var ws = function() { if (!tokens.length) return; if (tokens[0].type === "whitespace") { var t = tokens.shift(); t.value.replace(/\n/g, function(m) { line++; return m; }); return t; } }; var all_ws = function(store, pea) { // pea == post extended attribute, tpea = same for types var t = { type: "whitespace", value: "" }; while (true) { var w = ws(); if (!w) break; t.value += w.value; } if (t.value.length > 0) { if (store) { var w = t.value, re = { "ws": /^([\t\n\r ]+)/, "line-comment": /^\/\/(.*)\n?/m, "multiline-comment": /^\/\*((?:.|\n|\r)*?)\*\// }, wsTypes = []; for (var k in re) wsTypes.push(k); while (w.length) { var matched = false; for (var i = 0, n = wsTypes.length; i < n; i++) { var type = wsTypes[i]; w = w.replace(re[type], function(tok, m1) { store.push({ type: type + (pea ? ("-" + pea) : ""), value: m1 }); matched = true; return ""; }); if (matched) break; } if (matched) continue; throw new Error("Surprising white space construct."); // this shouldn't happen } } return t; } }; var integer_type = function() { var ret = ""; all_ws(); if (consume(ID, "unsigned")) ret = "unsigned "; all_ws(); if (consume(ID, "short")) return ret + "short"; if (consume(ID, "long")) { ret += "long"; all_ws(); if (consume(ID, "long")) return ret + " long"; return ret; } if (ret) error("Failed to parse integer type"); }; var float_type = function() { var ret = ""; all_ws(); if (consume(ID, "unrestricted")) ret = "unrestricted "; all_ws(); if (consume(ID, "float")) return ret + "float"; if (consume(ID, "double")) return ret + "double"; if (ret) error("Failed to parse float type"); }; var primitive_type = function() { var num_type = integer_type() || float_type(); if (num_type) return num_type; all_ws(); if (consume(ID, "boolean")) return "boolean"; if (consume(ID, "byte")) return "byte"; if (consume(ID, "octet")) return "octet"; }; var const_value = function() { if (consume(ID, "true")) return { type: "boolean", value: true }; if (consume(ID, "false")) return { type: "boolean", value: false }; if (consume(ID, "null")) return { type: "null" }; if (consume(ID, "Infinity")) return { type: "Infinity", negative: false }; if (consume(ID, "NaN")) return { type: "NaN" }; var ret = consume(FLOAT) || consume(INT); if (ret) return { type: "number", value: 1 * ret.value }; var tok = consume(OTHER, "-"); if (tok) { if (consume(ID, "Infinity")) return { type: "Infinity", negative: true }; else tokens.unshift(tok); } }; var type_suffix = function(obj) { while (true) { all_ws(); if (consume(OTHER, "?")) { if (obj.nullable) error("Can't nullable more than once"); obj.nullable = true; } else if (consume(OTHER, "[")) { all_ws(); consume(OTHER, "]") || error("Unterminated array type"); if (!obj.array) { obj.array = 1; obj.nullableArray = [obj.nullable]; } else { obj.array++; obj.nullableArray.push(obj.nullable); } obj.nullable = false; } else return; } }; var single_type = function() { var prim = primitive_type(), ret = { sequence: false, generic: null, nullable: false, array: false, union: false }, name, value; if (prim) { ret.idlType = prim; } else if (name = consume(ID)) { value = name.value; all_ws(); // Generic types if (consume(OTHER, "<")) { // backwards compat if (value === "sequence") { ret.sequence = true; } ret.generic = value; var types = []; do { all_ws(); types.push(type() || error("Error parsing generic type " + value)); all_ws(); } while (consume(OTHER, ",")); if (value === "sequence") { if (types.length !== 1) error("A sequence must have exactly one subtype"); } else if (value === "record") { if (types.length !== 2) error("A record must have exactly two subtypes"); if (!/^(DOMString|USVString|ByteString)$/.test(types[0].idlType)) { error("Record key must be DOMString, USVString, or ByteString"); } } ret.idlType = types.length === 1 ? types[0] : types; all_ws(); if (!consume(OTHER, ">")) error("Unterminated generic type " + value); type_suffix(ret); return ret; } else { ret.idlType = value; } } else { return; } type_suffix(ret); if (ret.nullable && !ret.array && ret.idlType === "any") error("Type any cannot be made nullable"); return ret; }; var union_type = function() { all_ws(); if (!consume(OTHER, "(")) return; var ret = { sequence: false, generic: null, nullable: false, array: false, union: true, idlType: [] }; var fst = type_with_extended_attributes() || error("Union type with no content"); ret.idlType.push(fst); while (true) { all_ws(); if (!consume(ID, "or")) break; var typ = type_with_extended_attributes() || error("No type after 'or' in union type"); ret.idlType.push(typ); } if (!consume(OTHER, ")")) error("Unterminated union type"); type_suffix(ret); return ret; }; var type = function() { return single_type() || union_type(); }; var type_with_extended_attributes = function() { var extAttrs = extended_attrs(); var ret = single_type() || union_type(); if (extAttrs.length && ret) ret.extAttrs = extAttrs; return ret; }; var argument = function(store) { var ret = { optional: false, variadic: false }; ret.extAttrs = extended_attrs(store); all_ws(store, "pea"); var opt_token = consume(ID, "optional"); if (opt_token) { ret.optional = true; all_ws(); } ret.idlType = type_with_extended_attributes(); if (!ret.idlType) { if (opt_token) tokens.unshift(opt_token); return; } var type_token = last_token; if (!ret.optional) { all_ws(); if (tokens.length >= 3 && tokens[0].type === "other" && tokens[0].value === "." && tokens[1].type === "other" && tokens[1].value === "." && tokens[2].type === "other" && tokens[2].value === "." ) { tokens.shift(); tokens.shift(); tokens.shift(); ret.variadic = true; } } all_ws(); var name = consume(ID); if (!name) { if (opt_token) tokens.unshift(opt_token); tokens.unshift(type_token); return; } ret.name = name.value; if (ret.optional) { all_ws(); var dflt = default_(); if (typeof dflt !== "undefined") { ret["default"] = dflt; } } return ret; }; var argument_list = function(store) { var ret = [], arg = argument(store ? ret : null); if (!arg) return; ret.push(arg); while (true) { all_ws(store ? ret : null); if (!consume(OTHER, ",")) return ret; var nxt = argument(store ? ret : null) || error("Trailing comma in arguments list"); ret.push(nxt); } }; var simple_extended_attr = function(store) { all_ws(); var name = consume(ID); if (!name) return; var ret = { name: name.value, "arguments": null }; all_ws(); var eq = consume(OTHER, "="); if (eq) { var rhs; all_ws(); if (rhs = consume(ID)) { ret.rhs = rhs; } else if (rhs = consume(FLOAT)) { ret.rhs = rhs; } else if (rhs = consume(INT)) { ret.rhs = rhs; } else if (rhs = consume(STR)) { ret.rhs = rhs; } else if (consume(OTHER, "(")) { // [Exposed=(Window,Worker)] rhs = []; var id = consume(ID); if (id) { rhs = [id.value]; } identifiers(rhs); consume(OTHER, ")") || error("Unexpected token in extended attribute argument list or type pair"); ret.rhs = { type: "identifier-list", value: rhs }; } if (!ret.rhs) return error("No right hand side to extended attribute assignment"); } all_ws(); if (consume(OTHER, "(")) { var args, pair; // [Constructor(DOMString str)] if (args = argument_list(store)) { ret["arguments"] = args; } // [Constructor()] else { ret["arguments"] = []; } all_ws(); consume(OTHER, ")") || error("Unexpected token in extended attribute argument list"); } return ret; }; // Note: we parse something simpler than the official syntax. It's all that ever // seems to be used var extended_attrs = function(store) { var eas = []; all_ws(store); if (!consume(OTHER, "[")) return eas; eas[0] = simple_extended_attr(store) || error("Extended attribute with not content"); all_ws(); while (consume(OTHER, ",")) { if (eas.length) { eas.push(simple_extended_attr(store)); } else { eas.push(simple_extended_attr(store) || error("Trailing comma in extended attribute")); } } consume(OTHER, "]") || error("No end of extended attribute"); return eas; }; var default_ = function() { all_ws(); if (consume(OTHER, "=")) { all_ws(); var def = const_value(); if (def) { return def; } else if (consume(OTHER, "[")) { if (!consume(OTHER, "]")) error("Default sequence value must be empty"); return { type: "sequence", value: [] }; } else { var str = consume(STR) || error("No value for default"); str.value = str.value.replace(/^"/, "").replace(/"$/, ""); return str; } } }; var const_ = function(store) { all_ws(store, "pea"); if (!consume(ID, "const")) return; var ret = { type: "const", nullable: false }; all_ws(); var typ = primitive_type(); if (!typ) { typ = consume(ID) || error("No type for const"); typ = typ.value; } ret.idlType = typ; all_ws(); if (consume(OTHER, "?")) { ret.nullable = true; all_ws(); } var name = consume(ID) || error("No name for const"); ret.name = name.value; all_ws(); consume(OTHER, "=") || error("No value assignment for const"); all_ws(); var cnt = const_value(); if (cnt) ret.value = cnt; else error("No value for const"); all_ws(); consume(OTHER, ";") || error("Unterminated const"); return ret; }; var inheritance = function() { all_ws(); if (consume(OTHER, ":")) { all_ws(); var inh = consume(ID) || error("No type in inheritance"); return inh.value; } }; var operation_rest = function(ret, store) { all_ws(); if (!ret) ret = {}; var name = consume(ID); ret.name = name ? name.value : null; all_ws(); consume(OTHER, "(") || error("Invalid operation"); ret["arguments"] = argument_list(store) || []; all_ws(); consume(OTHER, ")") || error("Unterminated operation"); all_ws(); consume(OTHER, ";") || error("Unterminated operation"); return ret; }; var callback = function(store) { all_ws(store, "pea"); var ret; if (!consume(ID, "callback")) return; all_ws(); var tok = consume(ID, "interface"); if (tok) { tokens.unshift(tok); ret = interface_(); ret.type = "callback interface"; return ret; } var name = consume(ID) || error("No name for callback"); ret = { type: "callback", name: name.value }; all_ws(); consume(OTHER, "=") || error("No assignment in callback"); all_ws(); ret.idlType = return_type(); all_ws(); consume(OTHER, "(") || error("No arguments in callback"); ret["arguments"] = argument_list(store) || []; all_ws(); consume(OTHER, ")") || error("Unterminated callback"); all_ws(); consume(OTHER, ";") || error("Unterminated callback"); return ret; }; var attribute = function(store) { all_ws(store, "pea"); var grabbed = [], ret = { type: "attribute", "static": false, stringifier: false, inherit: false, readonly: false }; if (consume(ID, "static")) { ret["static"] = true; grabbed.push(last_token); } else if (consume(ID, "stringifier")) { ret.stringifier = true; grabbed.push(last_token); } var w = all_ws(); if (w) grabbed.push(w); if (consume(ID, "inherit")) { if (ret["static"] || ret.stringifier) error("Cannot have a static or stringifier inherit"); ret.inherit = true; grabbed.push(last_token); var w = all_ws(); if (w) grabbed.push(w); } if (consume(ID, "readonly")) { ret.readonly = true; grabbed.push(last_token); var w = all_ws(); if (w) grabbed.push(w); } var rest = attribute_rest(ret); if (!rest) { tokens = grabbed.concat(tokens); } return rest; }; var attribute_rest = function(ret) { if (!consume(ID, "attribute")) { return; } all_ws(); ret.idlType = type_with_extended_attributes() || error("No type in attribute"); if (ret.idlType.sequence) error("Attributes cannot accept sequence types"); if (ret.idlType.generic === "record") error("Attributes cannot accept record types"); all_ws(); var name = consume(ID) || error("No name in attribute"); ret.name = name.value; all_ws(); consume(OTHER, ";") || error("Unterminated attribute"); return ret; }; var return_type = function() { var typ = type(); if (!typ) { if (consume(ID, "void")) { return "void"; } else error("No return type"); } return typ; }; var operation = function(store) { all_ws(store, "pea"); var ret = { type: "operation", getter: false, setter: false, creator: false, deleter: false, legacycaller: false, "static": false, stringifier: false }; while (true) { all_ws(); if (consume(ID, "getter")) ret.getter = true; else if (consume(ID, "setter")) ret.setter = true; else if (consume(ID, "creator")) ret.creator = true; else if (consume(ID, "deleter")) ret.deleter = true; else if (consume(ID, "legacycaller")) ret.legacycaller = true; else break; } if (ret.getter || ret.setter || ret.creator || ret.deleter || ret.legacycaller) { all_ws(); ret.idlType = return_type(); operation_rest(ret, store); return ret; } if (consume(ID, "static")) { ret["static"] = true; ret.idlType = return_type(); operation_rest(ret, store); return ret; } else if (consume(ID, "stringifier")) { ret.stringifier = true; - all_ws(); if (consume(OTHER, ";")) return ret; ret.idlType = return_type(); operation_rest(ret, store); return ret; } ret.idlType = return_type(); all_ws(); if (consume(ID, "iterator")) { all_ws(); ret.type = "iterator"; if (consume(ID, "object")) { ret.iteratorObject = "object"; } else if (consume(OTHER, "=")) { all_ws(); var name = consume(ID) || error("No right hand side in iterator"); ret.iteratorObject = name.value; } all_ws(); consume(OTHER, ";") || error("Unterminated iterator"); return ret; } else { operation_rest(ret, store); return ret; } }; var identifiers = function(arr) { while (true) { all_ws(); if (consume(OTHER, ",")) { all_ws(); var name = consume(ID) || error("Trailing comma in identifiers list"); arr.push(name.value); } else break; } }; var serialiser = function(store) { all_ws(store, "pea"); if (!consume(ID, "serializer")) return; var ret = { type: "serializer" }; all_ws(); if (consume(OTHER, "=")) { all_ws(); if (consume(OTHER, "{")) { ret.patternMap = true; all_ws(); var id = consume(ID); if (id && id.value === "getter") { ret.names = ["getter"]; } else if (id && id.value === "inherit") { ret.names = ["inherit"]; identifiers(ret.names); } else if (id) { ret.names = [id.value]; identifiers(ret.names); } else { ret.names = []; } all_ws(); consume(OTHER, "}") || error("Unterminated serializer pattern map"); } else if (consume(OTHER, "[")) { ret.patternList = true; all_ws(); var id = consume(ID); if (id && id.value === "getter") { ret.names = ["getter"]; } else if (id) { ret.names = [id.value]; identifiers(ret.names); } else { ret.names = []; } all_ws(); consume(OTHER, "]") || error("Unterminated serializer pattern list"); } else { var name = consume(ID) || error("Invalid serializer"); ret.name = name.value; } all_ws(); consume(OTHER, ";") || error("Unterminated serializer"); return ret; } else if (consume(OTHER, ";")) { // noop, just parsing } else { ret.idlType = return_type(); all_ws(); ret.operation = operation_rest(null, store); } return ret; }; var iterable_type = function() { if (consume(ID, "iterable")) return "iterable"; else if (consume(ID, "legacyiterable")) return "legacyiterable"; else if (consume(ID, "maplike")) return "maplike"; else if (consume(ID, "setlike")) return "setlike"; else return; }; var readonly_iterable_type = function() { if (consume(ID, "maplike")) return "maplike"; else if (consume(ID, "setlike")) return "setlike"; else return; }; var iterable = function(store) { all_ws(store, "pea"); var grabbed = [], ret = { type: null, idlType: null, readonly: false }; if (consume(ID, "readonly")) { ret.readonly = true; grabbed.push(last_token); var w = all_ws(); if (w) grabbed.push(w); } var consumeItType = ret.readonly ? readonly_iterable_type : iterable_type; var ittype = consumeItType(); if (!ittype) { tokens = grabbed.concat(tokens); return; } var secondTypeRequired = ittype === "maplike"; var secondTypeAllowed = secondTypeRequired || ittype === "iterable"; ret.type = ittype; if (ret.type !== 'maplike' && ret.type !== 'setlike') delete ret.readonly; all_ws(); if (consume(OTHER, "<")) { ret.idlType = type_with_extended_attributes() || error("Error parsing " + ittype + " declaration"); all_ws(); if (secondTypeAllowed) { var type2 = null; if (consume(OTHER, ",")) { all_ws(); type2 = type_with_extended_attributes(); all_ws(); } if (type2) ret.idlType = [ret.idlType, type2]; else if (secondTypeRequired) error("Missing second type argument in " + ittype + " declaration"); } if (!consume(OTHER, ">")) error("Unterminated " + ittype + " declaration"); all_ws(); if (!consume(OTHER, ";")) error("Missing semicolon after " + ittype + " declaration"); } else error("Error parsing " + ittype + " declaration"); return ret; }; var interface_ = function(isPartial, store) { all_ws(isPartial ? null : store, "pea"); if (!consume(ID, "interface")) return; all_ws(); var name = consume(ID) || error("No name for interface"); var mems = [], ret = { type: "interface", name: name.value, partial: false, members: mems }; if (!isPartial) ret.inheritance = inheritance() || null; all_ws(); consume(OTHER, "{") || error("Bodyless interface"); while (true) { all_ws(store ? mems : null); if (consume(OTHER, "}")) { all_ws(); consume(OTHER, ";") || error("Missing semicolon after interface"); return ret; } var ea = extended_attrs(store ? mems : null); all_ws(); var cnt = const_(store ? mems : null); if (cnt) { cnt.extAttrs = ea; ret.members.push(cnt); continue; } var mem = (opt.allowNestedTypedefs && typedef(store ? mems : null)) || iterable(store ? mems : null) || serialiser(store ? mems : null) || attribute(store ? mems : null) || operation(store ? mems : null) || error("Unknown member"); mem.extAttrs = ea; ret.members.push(mem); } }; var namespace = function(isPartial, store) { all_ws(isPartial ? null : store, "pea"); if (!consume(ID, "namespace")) return; all_ws(); var name = consume(ID) || error("No name for namespace"); var mems = [], ret = { type: "namespace", name: name.value, partial: isPartial, members: mems }; all_ws(); consume(OTHER, "{") || error("Bodyless namespace"); while (true) { all_ws(store ? mems : null); if (consume(OTHER, "}")) { all_ws(); consume(OTHER, ";") || error("Missing semicolon after namespace"); return ret; } var ea = extended_attrs(store ? mems : null); all_ws(); var mem = noninherited_attribute(store ? mems : null) || nonspecial_operation(store ? mems : null) || error("Unknown member"); mem.extAttrs = ea; ret.members.push(mem); } } var noninherited_attribute = function(store) { var w = all_ws(store, "pea"), grabbed = [], ret = { type: "attribute", "static": false, stringifier: false, inherit: false, readonly: false }; if (w) grabbed.push(w); if (consume(ID, "readonly")) { ret.readonly = true; grabbed.push(last_token); var w = all_ws(); if (w) grabbed.push(w); } var rest = attribute_rest(ret); if (!rest) { tokens = grabbed.concat(tokens); } return rest; } var nonspecial_operation = function(store) { all_ws(store, "pea"); var ret = { type: "operation", getter: false, setter: false, creator: false, deleter: false, legacycaller: false, "static": false, stringifier: false }; ret.idlType = return_type(); return operation_rest(ret, store); } var partial = function(store) { all_ws(store, "pea"); if (!consume(ID, "partial")) return; var thing = dictionary(true, store) || interface_(true, store) || namespace(true, store) || error("Partial doesn't apply to anything"); thing.partial = true; return thing; }; var dictionary = function(isPartial, store) { all_ws(isPartial ? null : store, "pea"); if (!consume(ID, "dictionary")) return; all_ws(); var name = consume(ID) || error("No name for dictionary"); var mems = [], ret = { type: "dictionary", name: name.value, partial: false, members: mems }; if (!isPartial) ret.inheritance = inheritance() || null; all_ws(); consume(OTHER, "{") || error("Bodyless dictionary"); while (true) { all_ws(store ? mems : null); if (consume(OTHER, "}")) { all_ws(); consume(OTHER, ";") || error("Missing semicolon after dictionary"); return ret; } var ea = extended_attrs(store ? mems : null); all_ws(store ? mems : null, "pea"); var required = consume(ID, "required"); var typ = type_with_extended_attributes() || error("No type for dictionary member"); all_ws(); var name = consume(ID) || error("No name for dictionary member"); var dflt = default_(); if (required && dflt) error("Required member must not have a default"); var member = { type: "field", name: name.value, required: !!required, idlType: typ, extAttrs: ea }; if (typeof dflt !== "undefined") { member["default"] = dflt; } ret.members.push(member); all_ws(); consume(OTHER, ";") || error("Unterminated dictionary member"); } }; var exception = function(store) { all_ws(store, "pea"); if (!consume(ID, "exception")) return; all_ws(); var name = consume(ID) || error("No name for exception"); var mems = [], ret = { type: "exception", name: name.value, members: mems }; ret.inheritance = inheritance() || null; all_ws(); consume(OTHER, "{") || error("Bodyless exception"); while (true) { all_ws(store ? mems : null); if (consume(OTHER, "}")) { all_ws(); consume(OTHER, ";") || error("Missing semicolon after exception"); return ret; } var ea = extended_attrs(store ? mems : null); all_ws(store ? mems : null, "pea"); var cnt = const_(); if (cnt) { cnt.extAttrs = ea; ret.members.push(cnt); } else { var typ = type(); all_ws(); var name = consume(ID); all_ws(); if (!typ || !name || !consume(OTHER, ";")) error("Unknown member in exception body"); ret.members.push({ type: "field", name: name.value, idlType: typ, extAttrs: ea }); } } }; var enum_ = function(store) { all_ws(store, "pea"); if (!consume(ID, "enum")) return; all_ws(); var name = consume(ID) || error("No name for enum"); var vals = [], ret = { type: "enum", name: name.value, values: vals }; all_ws(); consume(OTHER, "{") || error("No curly for enum"); var saw_comma = false; while (true) { all_ws(store ? vals : null); if (consume(OTHER, "}")) { all_ws(); consume(OTHER, ";") || error("No semicolon after enum"); return ret; } var val = consume(STR) || error("Unexpected value in enum"); ret.values.push(val.value.replace(/"/g, "")); all_ws(store ? vals : null); if (consume(OTHER, ",")) { if (store) vals.push({ type: "," }); all_ws(store ? vals : null); saw_comma = true; } else { saw_comma = false; } } }; var typedef = function(store) { all_ws(store, "pea"); if (!consume(ID, "typedef")) return; var ret = { type: "typedef" }; all_ws(); ret.idlType = type_with_extended_attributes() || error("No type in typedef"); all_ws(); var name = consume(ID) || error("No name in typedef"); ret.name = name.value; all_ws(); consume(OTHER, ";") || error("Unterminated typedef"); return ret; }; var implements_ = function(store) { all_ws(store, "pea"); var target = consume(ID); if (!target) return; var w = all_ws(); if (consume(ID, "implements")) { var ret = { type: "implements", target: target.value }; all_ws(); var imp = consume(ID) || error("Incomplete implements statement"); ret["implements"] = imp.value; all_ws(); consume(OTHER, ";") || error("No terminating ; for implements statement"); return ret; } else { // rollback tokens.unshift(w); tokens.unshift(target); } }; var definition = function(store) { return callback(store) || interface_(false, store) || partial(store) || dictionary(false, store) || exception(store) || enum_(store) || typedef(store) || implements_(store) || namespace(false, store); }; var definitions = function(store) { if (!tokens.length) return []; var defs = []; while (true) { var ea = extended_attrs(store ? defs : null), def = definition(store ? defs : null); if (!def) { if (ea.length) error("Stray extended attributes"); break; } def.extAttrs = ea; defs.push(def); } return defs; }; var res = definitions(opt.ws); if (tokens.length) error("Unrecognised tokens"); return res; }; var obj = { parse: function(str, opt) { if (!opt) opt = {}; var tokens = tokenise(str); return parse(tokens, opt); } }; if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') { module.exports = obj; } else if (typeof define === 'function' && define.amd) { define([], function() { return obj; }); } else { (self || window).WebIDL2 = obj; } // HACK: force WebIDL2 global if(typeof window !== 'undefined') { window.WebIDL2 = obj; } }()); jsonld.js-4.0.1/tests/webidl/fetch-latest000077500000000000000000000010411401135163200203310ustar00rootroot00000000000000#!/bin/sh # Fetch latest files from github. # https://github.com/w3c/web-platform-tests/tree/master/resources # https://github.com/w3c/web-platform-tests/tree/master/resources/webidl2/lib curl -o testharness.js-new https://raw.githubusercontent.com/w3c/web-platform-tests/master/resources/testharness.js curl -o idlharness.js-new https://raw.githubusercontent.com/w3c/web-platform-tests/master/resources/idlharness.js curl -o WebIDLParser.js-new https://raw.githubusercontent.com/w3c/web-platform-tests/master/resources/webidl2/lib/webidl2.js jsonld.js-4.0.1/tests/webidl/idlharness.js000066400000000000000000002767431401135163200205410ustar00rootroot00000000000000/* Distributed under both the W3C Test Suite License [1] and the W3C 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the policies and contribution forms [3]. [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license [3] http://www.w3.org/2004/10/27-testcases */ /* For user documentation see docs/_writing-tests/idlharness.md */ /** * Notes for people who want to edit this file (not just use it as a library): * * Most of the interesting stuff happens in the derived classes of IdlObject, * especially IdlInterface. The entry point for all IdlObjects is .test(), * which is called by IdlArray.test(). An IdlObject is conceptually just * "thing we want to run tests on", and an IdlArray is an array of IdlObjects * with some additional data thrown in. * * The object model is based on what WebIDLParser.js produces, which is in turn * based on its pegjs grammar. If you want to figure out what properties an * object will have from WebIDLParser.js, the best way is to look at the * grammar: * * https://github.com/darobin/webidl.js/blob/master/lib/grammar.peg * * So for instance: * * // interface definition * interface * = extAttrs:extendedAttributeList? S? "interface" S name:identifier w herit:ifInheritance? w "{" w mem:ifMember* w "}" w ";" w * { return { type: "interface", name: name, inheritance: herit, members: mem, extAttrs: extAttrs }; } * * This means that an "interface" object will have a .type property equal to * the string "interface", a .name property equal to the identifier that the * parser found, an .inheritance property equal to either null or the result of * the "ifInheritance" production found elsewhere in the grammar, and so on. * After each grammatical production is a JavaScript function in curly braces * that gets called with suitable arguments and returns some JavaScript value. * * (Note that the version of WebIDLParser.js we use might sometimes be * out-of-date or forked.) * * The members and methods of the classes defined by this file are all at least * briefly documented, hopefully. */ (function(){ "use strict"; /// Helpers /// function constValue (cnt) //@{ { if (cnt.type === "null") return null; if (cnt.type === "NaN") return NaN; if (cnt.type === "Infinity") return cnt.negative ? -Infinity : Infinity; return cnt.value; } //@} function minOverloadLength(overloads) //@{ { if (!overloads.length) { return 0; } return overloads.map(function(attr) { return attr.arguments ? attr.arguments.filter(function(arg) { return !arg.optional && !arg.variadic; }).length : 0; }) .reduce(function(m, n) { return Math.min(m, n); }); } //@} function throwOrReject(a_test, operation, fn, obj, args, message, cb) //@{ { if (operation.idlType.generic !== "Promise") { assert_throws(new TypeError(), function() { fn.apply(obj, args); }, message); cb(); } else { try { promise_rejects(a_test, new TypeError(), fn.apply(obj, args), message).then(cb, cb); } catch (e){ a_test.step(function() { assert_unreached("Throws \"" + e + "\" instead of rejecting promise"); cb(); }); } } } //@} function awaitNCallbacks(n, cb, ctx) //@{ { var counter = 0; return function() { counter++; if (counter >= n) { cb(); } }; } //@} var fround = //@{ (function(){ if (Math.fround) return Math.fround; var arr = new Float32Array(1); return function fround(n) { arr[0] = n; return arr[0]; }; })(); //@} /// IdlArray /// // Entry point self.IdlArray = function() //@{ { /** * A map from strings to the corresponding named IdlObject, such as * IdlInterface or IdlException. These are the things that test() will run * tests on. */ this.members = {}; /** * A map from strings to arrays of strings. The keys are interface or * exception names, and are expected to also exist as keys in this.members * (otherwise they'll be ignored). This is populated by add_objects() -- * see documentation at the start of the file. The actual tests will be * run by calling this.members[name].test_object(obj) for each obj in * this.objects[name]. obj is a string that will be eval'd to produce a * JavaScript value, which is supposed to be an object implementing the * given IdlObject (interface, exception, etc.). */ this.objects = {}; /** * When adding multiple collections of IDLs one at a time, an earlier one * might contain a partial interface or implements statement that depends * on a later one. Save these up and handle them right before we run * tests. * * .partials is simply an array of objects from WebIDLParser.js' * "partialinterface" production. .implements maps strings to arrays of * strings, such that * * A implements B; * A implements C; * D implements E; * * results in { A: ["B", "C"], D: ["E"] }. */ this.partials = []; this["implements"] = {}; }; //@} IdlArray.prototype.add_idls = function(raw_idls) //@{ { /** Entry point. See documentation at beginning of file. */ this.internal_add_idls(WebIDL2.parse(raw_idls)); }; //@} IdlArray.prototype.add_untested_idls = function(raw_idls) //@{ { /** Entry point. See documentation at beginning of file. */ var parsed_idls = WebIDL2.parse(raw_idls); for (var i = 0; i < parsed_idls.length; i++) { parsed_idls[i].untested = true; if ("members" in parsed_idls[i]) { for (var j = 0; j < parsed_idls[i].members.length; j++) { parsed_idls[i].members[j].untested = true; } } } this.internal_add_idls(parsed_idls); }; //@} IdlArray.prototype.internal_add_idls = function(parsed_idls) //@{ { /** * Internal helper called by add_idls() and add_untested_idls(). * parsed_idls is an array of objects that come from WebIDLParser.js's * "definitions" production. The add_untested_idls() entry point * additionally sets an .untested property on each object (and its * .members) so that they'll be skipped by test() -- they'll only be * used for base interfaces of tested interfaces, return types, etc. */ parsed_idls.forEach(function(parsed_idl) { if (parsed_idl.type == "interface" && parsed_idl.partial) { this.partials.push(parsed_idl); return; } if (parsed_idl.type == "implements") { if (!(parsed_idl.target in this["implements"])) { this["implements"][parsed_idl.target] = []; } this["implements"][parsed_idl.target].push(parsed_idl["implements"]); return; } parsed_idl.array = this; if (parsed_idl.name in this.members) { throw "Duplicate identifier " + parsed_idl.name; } switch(parsed_idl.type) { case "interface": this.members[parsed_idl.name] = new IdlInterface(parsed_idl, /* is_callback = */ false); break; case "dictionary": // Nothing to test, but we need the dictionary info around for type // checks this.members[parsed_idl.name] = new IdlDictionary(parsed_idl); break; case "typedef": this.members[parsed_idl.name] = new IdlTypedef(parsed_idl); break; case "callback": // TODO console.log("callback not yet supported"); break; case "enum": this.members[parsed_idl.name] = new IdlEnum(parsed_idl); break; case "callback interface": this.members[parsed_idl.name] = new IdlInterface(parsed_idl, /* is_callback = */ true); break; default: throw parsed_idl.name + ": " + parsed_idl.type + " not yet supported"; } }.bind(this)); }; //@} IdlArray.prototype.add_objects = function(dict) //@{ { /** Entry point. See documentation at beginning of file. */ for (var k in dict) { if (k in this.objects) { this.objects[k] = this.objects[k].concat(dict[k]); } else { this.objects[k] = dict[k]; } } }; //@} IdlArray.prototype.prevent_multiple_testing = function(name) //@{ { /** Entry point. See documentation at beginning of file. */ this.members[name].prevent_multiple_testing = true; }; //@} IdlArray.prototype.recursively_get_implements = function(interface_name) //@{ { /** * Helper function for test(). Returns an array of things that implement * interface_name, so if the IDL contains * * A implements B; * B implements C; * B implements D; * * then recursively_get_implements("A") should return ["B", "C", "D"]. */ var ret = this["implements"][interface_name]; if (ret === undefined) { return []; } for (var i = 0; i < this["implements"][interface_name].length; i++) { ret = ret.concat(this.recursively_get_implements(ret[i])); if (ret.indexOf(ret[i]) != ret.lastIndexOf(ret[i])) { throw "Circular implements statements involving " + ret[i]; } } return ret; }; //@} IdlArray.prototype.is_json_type = function(type) //@{ { /** * Checks whether type is a JSON type as per * https://heycam.github.io/webidl/#dfn-json-types */ var idlType = type.idlType; if (type.generic == "Promise") { return false; } // nullable and annotated types don't need to be handled separately, // as webidl2 doesn't represent them wrapped-up (as they're described // in WebIDL). // union and record types if (type.union || type.generic == "record") { return idlType.every(this.is_json_type, this); } // sequence types if (type.generic == "sequence" || type.generic == "FrozenArray") { return this.is_json_type(idlType); } if (typeof idlType != "string") { throw new Error("Unexpected type " + JSON.stringify(idlType)); } switch (idlType) { // Numeric types case "byte": case "octet": case "short": case "unsigned short": case "long": case "unsigned long": case "long long": case "unsigned long long": case "float": case "double": case "unrestricted float": case "unrestricted double": // boolean case "boolean": // string types case "DOMString": case "ByteString": case "USVString": // object type case "object": return true; case "Error": case "DOMException": case "Int8Array": case "Int16Array": case "Int32Array": case "Uint8Array": case "Uint16Array": case "Uint32Array": case "Uint8ClampedArray": case "Float32Array": case "ArrayBuffer": case "DataView": case "any": return false; default: var thing = this.members[idlType]; if (!thing) { throw new Error("Type " + idlType + " not found"); } if (thing instanceof IdlEnum) { return true; } if (thing instanceof IdlTypedef) { return this.is_json_type(thing.idlType); } // dictionaries where all of their members are JSON types if (thing instanceof IdlDictionary) { var stack = thing.get_inheritance_stack(); var map = new Map(); while (stack.length) { stack.pop().members.forEach(function(m) { map.set(m.name, m.idlType) }); } return Array.from(map.values()).every(this.is_json_type, this); } // interface types that have a toJSON operation declared on themselves or // one of their inherited or consequential interfaces. if (thing instanceof IdlInterface) { var base; while (thing) { if (thing.has_to_json_regular_operation()) { return true; } var mixins = this.implements[thing.name]; if (mixins) { mixins = mixins.map(function(id) { var mixin = this.members[id]; if (!mixin) { throw new Error("Interface " + id + " not found (implemented by " + thing.name + ")"); } return mixin; }, this); if (mixins.some(function(m) { return m.has_to_json_regular_operation() } )) { return true; } } if (!thing.base) { return false; } base = this.members[thing.base]; if (!base) { throw new Error("Interface " + thing.base + " not found (inherited by " + thing.name + ")"); } thing = base; } return false; } return false; } }; function exposure_set(object, default_set) { var exposed = object.extAttrs.filter(function(a) { return a.name == "Exposed" }); if (exposed.length > 1 || exposed.length < 0) { throw "Unexpected Exposed extended attributes on " + memberName + ": " + exposed; } if (exposed.length === 0) { return default_set; } var set = exposed[0].rhs.value; // Could be a list or a string. if (typeof set == "string") { set = [ set ]; } return set; } function exposed_in(globals) { if ('document' in self) { return globals.indexOf("Window") >= 0; } if ('DedicatedWorkerGlobalScope' in self && self instanceof DedicatedWorkerGlobalScope) { return globals.indexOf("Worker") >= 0 || globals.indexOf("DedicatedWorker") >= 0; } if ('SharedWorkerGlobalScope' in self && self instanceof SharedWorkerGlobalScope) { return globals.indexOf("Worker") >= 0 || globals.indexOf("SharedWorker") >= 0; } if ('ServiceWorkerGlobalScope' in self && self instanceof ServiceWorkerGlobalScope) { return globals.indexOf("Worker") >= 0 || globals.indexOf("ServiceWorker") >= 0; } throw "Unexpected global object"; } //@} IdlArray.prototype.test = function() //@{ { /** Entry point. See documentation at beginning of file. */ // First merge in all the partial interfaces and implements statements we // encountered. this.partials.forEach(function(parsed_idl) { if (!(parsed_idl.name in this.members) || !(this.members[parsed_idl.name] instanceof IdlInterface)) { throw "Partial interface " + parsed_idl.name + " with no original interface"; } if (parsed_idl.extAttrs) { parsed_idl.extAttrs.forEach(function(extAttr) { this.members[parsed_idl.name].extAttrs.push(extAttr); }.bind(this)); } parsed_idl.members.forEach(function(member) { this.members[parsed_idl.name].members.push(new IdlInterfaceMember(member)); }.bind(this)); }.bind(this)); this.partials = []; for (var lhs in this["implements"]) { this.recursively_get_implements(lhs).forEach(function(rhs) { var errStr = lhs + " implements " + rhs + ", but "; if (!(lhs in this.members)) throw errStr + lhs + " is undefined."; if (!(this.members[lhs] instanceof IdlInterface)) throw errStr + lhs + " is not an interface."; if (!(rhs in this.members)) throw errStr + rhs + " is undefined."; if (!(this.members[rhs] instanceof IdlInterface)) throw errStr + rhs + " is not an interface."; this.members[rhs].members.forEach(function(member) { this.members[lhs].members.push(new IdlInterfaceMember(member)); }.bind(this)); }.bind(this)); } this["implements"] = {}; Object.getOwnPropertyNames(this.members).forEach(function(memberName) { var member = this.members[memberName]; if (!(member instanceof IdlInterface)) { return; } var globals = exposure_set(member, ["Window"]); member.exposed = exposed_in(globals); member.exposureSet = globals; }.bind(this)); // Now run test() on every member, and test_object() for every object. for (var name in this.members) { this.members[name].test(); if (name in this.objects) { this.objects[name].forEach(function(str) { this.members[name].test_object(str); }.bind(this)); } } }; //@} IdlArray.prototype.assert_type_is = function(value, type) //@{ { if (type.idlType in this.members && this.members[type.idlType] instanceof IdlTypedef) { this.assert_type_is(value, this.members[type.idlType].idlType); return; } if (type.union) { for (var i = 0; i < type.idlType.length; i++) { try { this.assert_type_is(value, type.idlType[i]); // No AssertionError, so we match one type in the union return; } catch(e) { if (e instanceof AssertionError) { // We didn't match this type, let's try some others continue; } throw e; } } // TODO: Is there a nice way to list the union's types in the message? assert_true(false, "Attribute has value " + format_value(value) + " which doesn't match any of the types in the union"); } /** * Helper function that tests that value is an instance of type according * to the rules of WebIDL. value is any JavaScript value, and type is an * object produced by WebIDLParser.js' "type" production. That production * is fairly elaborate due to the complexity of WebIDL's types, so it's * best to look at the grammar to figure out what properties it might have. */ if (type.idlType == "any") { // No assertions to make return; } if (type.nullable && value === null) { // This is fine return; } if (type.array) { // TODO: not supported yet return; } if (type.sequence) { assert_true(Array.isArray(value), "should be an Array"); if (!value.length) { // Nothing we can do. return; } this.assert_type_is(value[0], type.idlType); return; } if (type.generic === "Promise") { assert_true("then" in value, "Attribute with a Promise type should have a then property"); // TODO: Ideally, we would check on project fulfillment // that we get the right type // but that would require making the type check async return; } if (type.generic === "FrozenArray") { assert_true(Array.isArray(value), "Value should be array"); assert_true(Object.isFrozen(value), "Value should be frozen"); if (!value.length) { // Nothing we can do. return; } this.assert_type_is(value[0], type.idlType); return; } type = type.idlType; switch(type) { case "void": assert_equals(value, undefined); return; case "boolean": assert_equals(typeof value, "boolean"); return; case "byte": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-128 <= value && value <= 127, "byte " + value + " should be in range [-128, 127]"); return; case "octet": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 255, "octet " + value + " should be in range [0, 255]"); return; case "short": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-32768 <= value && value <= 32767, "short " + value + " should be in range [-32768, 32767]"); return; case "unsigned short": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 65535, "unsigned short " + value + " should be in range [0, 65535]"); return; case "long": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(-2147483648 <= value && value <= 2147483647, "long " + value + " should be in range [-2147483648, 2147483647]"); return; case "unsigned long": assert_equals(typeof value, "number"); assert_equals(value, Math.floor(value), "should be an integer"); assert_true(0 <= value && value <= 4294967295, "unsigned long " + value + " should be in range [0, 4294967295]"); return; case "long long": assert_equals(typeof value, "number"); return; case "unsigned long long": case "DOMTimeStamp": assert_equals(typeof value, "number"); assert_true(0 <= value, "unsigned long long should be positive"); return; case "float": assert_equals(typeof value, "number"); assert_equals(value, fround(value), "float rounded to 32-bit float should be itself"); assert_not_equals(value, Infinity); assert_not_equals(value, -Infinity); assert_not_equals(value, NaN); return; case "DOMHighResTimeStamp": case "double": assert_equals(typeof value, "number"); assert_not_equals(value, Infinity); assert_not_equals(value, -Infinity); assert_not_equals(value, NaN); return; case "unrestricted float": assert_equals(typeof value, "number"); assert_equals(value, fround(value), "unrestricted float rounded to 32-bit float should be itself"); return; case "unrestricted double": assert_equals(typeof value, "number"); return; case "DOMString": assert_equals(typeof value, "string"); return; case "ByteString": assert_equals(typeof value, "string"); assert_regexp_match(value, /^[\x00-\x7F]*$/); return; case "USVString": assert_equals(typeof value, "string"); assert_regexp_match(value, /^([\x00-\ud7ff\ue000-\uffff]|[\ud800-\udbff][\udc00-\udfff])*$/); return; case "object": assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); return; } if (!(type in this.members)) { throw "Unrecognized type " + type; } if (this.members[type] instanceof IdlInterface) { // We don't want to run the full // IdlInterface.prototype.test_instance_of, because that could result // in an infinite loop. TODO: This means we don't have tests for // NoInterfaceObject interfaces, and we also can't test objects that // come from another self. assert_in_array(typeof value, ["object", "function"], "wrong type: not object or function"); if (value instanceof Object && !this.members[type].has_extended_attribute("NoInterfaceObject") && type in self) { assert_true(value instanceof self[type], "instanceof " + type); } } else if (this.members[type] instanceof IdlEnum) { assert_equals(typeof value, "string"); } else if (this.members[type] instanceof IdlDictionary) { // TODO: Test when we actually have something to test this on } else { throw "Type " + type + " isn't an interface or dictionary"; } }; //@} /// IdlObject /// function IdlObject() {} IdlObject.prototype.test = function() //@{ { /** * By default, this does nothing, so no actual tests are run for IdlObjects * that don't define any (e.g., IdlDictionary at the time of this writing). */ }; //@} IdlObject.prototype.has_extended_attribute = function(name) //@{ { /** * This is only meaningful for things that support extended attributes, * such as interfaces, exceptions, and members. */ return this.extAttrs.some(function(o) { return o.name == name; }); }; //@} /// IdlDictionary /// // Used for IdlArray.prototype.assert_type_is function IdlDictionary(obj) //@{ { /** * obj is an object produced by the WebIDLParser.js "dictionary" * production. */ /** Self-explanatory. */ this.name = obj.name; /** A back-reference to our IdlArray. */ this.array = obj.array; /** An array of objects produced by the "dictionaryMember" production. */ this.members = obj.members; /** * The name (as a string) of the dictionary type we inherit from, or null * if there is none. */ this.base = obj.inheritance; } //@} IdlDictionary.prototype = Object.create(IdlObject.prototype); IdlDictionary.prototype.get_inheritance_stack = function() { return IdlInterface.prototype.get_inheritance_stack.call(this); }; /// IdlInterface /// function IdlInterface(obj, is_callback) //@{ { /** * obj is an object produced by the WebIDLParser.js "interface" production. */ /** Self-explanatory. */ this.name = obj.name; /** A back-reference to our IdlArray. */ this.array = obj.array; /** * An indicator of whether we should run tests on the interface object and * interface prototype object. Tests on members are controlled by .untested * on each member, not this. */ this.untested = obj.untested; /** An array of objects produced by the "ExtAttr" production. */ this.extAttrs = obj.extAttrs; /** An array of IdlInterfaceMembers. */ this.members = obj.members.map(function(m){return new IdlInterfaceMember(m); }); if (this.has_extended_attribute("Unforgeable")) { this.members .filter(function(m) { return !m["static"] && (m.type == "attribute" || m.type == "operation"); }) .forEach(function(m) { return m.isUnforgeable = true; }); } /** * The name (as a string) of the type we inherit from, or null if there is * none. */ this.base = obj.inheritance; this._is_callback = is_callback; } //@} IdlInterface.prototype = Object.create(IdlObject.prototype); IdlInterface.prototype.is_callback = function() //@{ { return this._is_callback; }; //@} IdlInterface.prototype.has_constants = function() //@{ { return this.members.some(function(member) { return member.type === "const"; }); }; //@} IdlInterface.prototype.is_global = function() //@{ { return this.extAttrs.some(function(attribute) { return attribute.name === "Global" || attribute.name === "PrimaryGlobal"; }); }; //@} IdlInterface.prototype.has_to_json_regular_operation = function() { return this.members.some(function(m) { return m.is_to_json_regular_operation(); }); }; IdlInterface.prototype.has_default_to_json_regular_operation = function() { return this.members.some(function(m) { return m.is_to_json_regular_operation() && m.has_extended_attribute("Default"); }); }; IdlInterface.prototype.get_inheritance_stack = function() { /** * See https://heycam.github.io/webidl/#create-an-inheritance-stack * * Returns an array of IdlInterface objects which contains itself * and all of its inherited interfaces. * * So given: * * A : B {}; * B : C {}; * C {}; * * then A.get_inheritance_stack() should return [A, B, C], * and B.get_inheritance_stack() should return [B, C]. * * Note: as dictionary inheritance is expressed identically by the AST, * this works just as well for getting a stack of inherited dictionaries. */ var stack = [this]; var idl_interface = this; while (idl_interface.base) { var base = this.array.members[idl_interface.base]; if (!base) { throw new Error(idl_interface.type + " " + idl_interface.base + " not found (inherited by " + idl_interface.name + ")"); } idl_interface = base; stack.push(idl_interface); } return stack; }; /** * Implementation of * https://heycam.github.io/webidl/#default-tojson-operation * for testing purposes. * * Collects the IDL types of the attributes that meet the criteria * for inclusion in the default toJSON operation for easy * comparison with actual value */ IdlInterface.prototype.default_to_json_operation = function(callback) { var map = new Map(), isDefault = false; this.traverse_inherited_and_consequential_interfaces(function(I) { if (I.has_default_to_json_regular_operation()) { isDefault = true; I.members.forEach(function(m) { if (!m.static && m.type == "attribute" && I.array.is_json_type(m.idlType)) { map.set(m.name, m.idlType); } }); } else if (I.has_to_json_regular_operation()) { isDefault = false; } }); return isDefault ? map : null; }; /** * Traverses inherited interfaces from the top down * and imeplemented interfaces inside out. * Invokes |callback| on each interface. * * This is an abstract implementation of the traversal * algorithm specified in: * https://heycam.github.io/webidl/#collect-attribute-values * Given the following inheritance tree: * * F * | * C E - I * | | * B - D * | * G - A - H - J * * Invoking traverse_inherited_and_consequential_interfaces() on A * would traverse the tree in the following order: * C -> B -> F -> E -> I -> D -> A -> G -> H -> J */ IdlInterface.prototype.traverse_inherited_and_consequential_interfaces = function(callback) { if (typeof callback != "function") { throw new TypeError(); } var stack = this.get_inheritance_stack(); _traverse_inherited_and_consequential_interfaces(stack, callback); }; function _traverse_inherited_and_consequential_interfaces(stack, callback) { var I = stack.pop(); callback(I); var mixins = I.array["implements"][I.name]; if (mixins) { mixins.forEach(function(id) { var mixin = I.array.members[id]; if (!mixin) { throw new Error("Interface " + id + " not found (implemented by " + I.name + ")"); } var interfaces = mixin.get_inheritance_stack(); _traverse_inherited_and_consequential_interfaces(interfaces, callback); }); } if (stack.length > 0) { _traverse_inherited_and_consequential_interfaces(stack, callback); } } IdlInterface.prototype.test = function() //@{ { if (this.has_extended_attribute("NoInterfaceObject")) { // No tests to do without an instance. TODO: We should still be able // to run tests on the prototype object, if we obtain one through some // other means. return; } if (!this.exposed) { test(function() { assert_false(this.name in self); }.bind(this), this.name + " interface: existence and properties of interface object"); return; } if (!this.untested) { // First test things to do with the exception/interface object and // exception/interface prototype object. this.test_self(); } // Then test things to do with its members (constants, fields, attributes, // operations, . . .). These are run even if .untested is true, because // members might themselves be marked as .untested. This might happen to // interfaces if the interface itself is untested but a partial interface // that extends it is tested -- then the interface itself and its initial // members will be marked as untested, but the members added by the partial // interface are still tested. this.test_members(); }; //@} IdlInterface.prototype.test_self = function() //@{ { test(function() { // This function tests WebIDL as of 2015-01-13. // "For every interface that is exposed in a given ECMAScript global // environment and: // * is a callback interface that has constants declared on it, or // * is a non-callback interface that is not declared with the // [NoInterfaceObject] extended attribute, // a corresponding property MUST exist on the ECMAScript global object. // The name of the property is the identifier of the interface, and its // value is an object called the interface object. // The property has the attributes { [[Writable]]: true, // [[Enumerable]]: false, [[Configurable]]: true }." if (this.is_callback() && !this.has_constants()) { return; } // TODO: Should we test here that the property is actually writable // etc., or trust getOwnPropertyDescriptor? assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); var desc = Object.getOwnPropertyDescriptor(self, this.name); assert_false("get" in desc, "self's property " + format_value(this.name) + " should not have a getter"); assert_false("set" in desc, "self's property " + format_value(this.name) + " should not have a setter"); assert_true(desc.writable, "self's property " + format_value(this.name) + " should be writable"); assert_false(desc.enumerable, "self's property " + format_value(this.name) + " should not be enumerable"); assert_true(desc.configurable, "self's property " + format_value(this.name) + " should be configurable"); if (this.is_callback()) { // "The internal [[Prototype]] property of an interface object for // a callback interface must be the Function.prototype object." assert_equals(Object.getPrototypeOf(self[this.name]), Function.prototype, "prototype of self's property " + format_value(this.name) + " is not Object.prototype"); return; } // "The interface object for a given non-callback interface is a // function object." // "If an object is defined to be a function object, then it has // characteristics as follows:" // Its [[Prototype]] internal property is otherwise specified (see // below). // "* Its [[Get]] internal property is set as described in ECMA-262 // section 9.1.8." // Not much to test for this. // "* Its [[Construct]] internal property is set as described in // ECMA-262 section 19.2.2.3." // Tested below if no constructor is defined. TODO: test constructors // if defined. // "* Its @@hasInstance property is set as described in ECMA-262 // section 19.2.3.8, unless otherwise specified." // TODO // ES6 (rev 30) 19.1.3.6: // "Else, if O has a [[Call]] internal method, then let builtinTag be // "Function"." assert_class_string(self[this.name], "Function", "class string of " + this.name); // "The [[Prototype]] internal property of an interface object for a // non-callback interface is determined as follows:" var prototype = Object.getPrototypeOf(self[this.name]); if (this.base) { // "* If the interface inherits from some other interface, the // value of [[Prototype]] is the interface object for that other // interface." var has_interface_object = !this.array .members[this.base] .has_extended_attribute("NoInterfaceObject"); if (has_interface_object) { assert_own_property(self, this.base, 'should inherit from ' + this.base + ', but self has no such property'); assert_equals(prototype, self[this.base], 'prototype of ' + this.name + ' is not ' + this.base); } } else { // "If the interface doesn't inherit from any other interface, the // value of [[Prototype]] is %FunctionPrototype% ([ECMA-262], // section 6.1.7.4)." assert_equals(prototype, Function.prototype, "prototype of self's property " + format_value(this.name) + " is not Function.prototype"); } if (!this.has_extended_attribute("Constructor")) { // "The internal [[Call]] method of the interface object behaves as // follows . . . // // "If I was not declared with a [Constructor] extended attribute, // then throw a TypeError." assert_throws(new TypeError(), function() { self[this.name](); }.bind(this), "interface object didn't throw TypeError when called as a function"); assert_throws(new TypeError(), function() { new self[this.name](); }.bind(this), "interface object didn't throw TypeError when called as a constructor"); } }.bind(this), this.name + " interface: existence and properties of interface object"); if (!this.is_callback()) { test(function() { // This function tests WebIDL as of 2014-10-25. // https://heycam.github.io/webidl/#es-interface-call assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); // "Interface objects for non-callback interfaces MUST have a // property named “length” with attributes { [[Writable]]: false, // [[Enumerable]]: false, [[Configurable]]: true } whose value is // a Number." assert_own_property(self[this.name], "length"); var desc = Object.getOwnPropertyDescriptor(self[this.name], "length"); assert_false("get" in desc, this.name + ".length should not have a getter"); assert_false("set" in desc, this.name + ".length should not have a setter"); assert_false(desc.writable, this.name + ".length should not be writable"); assert_false(desc.enumerable, this.name + ".length should not be enumerable"); assert_true(desc.configurable, this.name + ".length should be configurable"); var constructors = this.extAttrs .filter(function(attr) { return attr.name == "Constructor"; }); var expected_length = minOverloadLength(constructors); assert_equals(self[this.name].length, expected_length, "wrong value for " + this.name + ".length"); }.bind(this), this.name + " interface object length"); } if (!this.is_callback() || this.has_constants()) { test(function() { // This function tests WebIDL as of 2015-11-17. // https://heycam.github.io/webidl/#interface-object assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); // "All interface objects must have a property named “name” with // attributes { [[Writable]]: false, [[Enumerable]]: false, // [[Configurable]]: true } whose value is the identifier of the // corresponding interface." assert_own_property(self[this.name], "name"); var desc = Object.getOwnPropertyDescriptor(self[this.name], "name"); assert_false("get" in desc, this.name + ".name should not have a getter"); assert_false("set" in desc, this.name + ".name should not have a setter"); assert_false(desc.writable, this.name + ".name should not be writable"); assert_false(desc.enumerable, this.name + ".name should not be enumerable"); assert_true(desc.configurable, this.name + ".name should be configurable"); assert_equals(self[this.name].name, this.name, "wrong value for " + this.name + ".name"); }.bind(this), this.name + " interface object name"); } if (this.has_extended_attribute("LegacyWindowAlias")) { test(function() { var aliasAttrs = this.extAttrs.filter(function(o) { return o.name === "LegacyWindowAlias"; }); if (aliasAttrs.length > 1) { throw "Invalid IDL: multiple LegacyWindowAlias extended attributes on " + this.name; } if (this.is_callback()) { throw "Invalid IDL: LegacyWindowAlias extended attribute on non-interface " + this.name; } if (this.exposureSet.indexOf("Window") === -1) { throw "Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " which is not exposed in Window"; } // TODO: when testing of [NoInterfaceObject] interfaces is supported, // check that it's not specified together with LegacyWindowAlias. // TODO: maybe check that [LegacyWindowAlias] is not specified on a partial interface. var rhs = aliasAttrs[0].rhs; if (!rhs) { throw "Invalid IDL: LegacyWindowAlias extended attribute on " + this.name + " without identifier"; } var aliases; if (rhs.type === "identifier-list") { aliases = rhs.value; } else { // rhs.type === identifier aliases = [ rhs.value ]; } // OK now actually check the aliases... var alias; if (exposed_in(exposure_set(this, this.exposureSet)) && 'document' in self) { for (alias of aliases) { assert_true(alias in self, alias + " should exist"); assert_equals(self[alias], self[this.name], "self." + alias + " should be the same value as self." + this.name); var desc = Object.getOwnPropertyDescriptor(self, alias); assert_equals(desc.value, self[this.name], "wrong value in " + alias + " property descriptor"); assert_true(desc.writable, alias + " should be writable"); assert_false(desc.enumerable, alias + " should not be enumerable"); assert_true(desc.configurable, alias + " should be configurable"); assert_false('get' in desc, alias + " should not have a getter"); assert_false('set' in desc, alias + " should not have a setter"); } } else { for (alias of aliases) { assert_false(alias in self, alias + " should not exist"); } } }.bind(this), this.name + " interface: legacy window alias"); } // TODO: Test named constructors if I find any interfaces that have them. test(function() { // This function tests WebIDL as of 2015-01-21. // https://heycam.github.io/webidl/#interface-object if (this.is_callback() && !this.has_constants()) { return; } assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); if (this.is_callback()) { assert_false("prototype" in self[this.name], this.name + ' should not have a "prototype" property'); return; } // "An interface object for a non-callback interface must have a // property named “prototype” with attributes { [[Writable]]: false, // [[Enumerable]]: false, [[Configurable]]: false } whose value is an // object called the interface prototype object. This object has // properties that correspond to the regular attributes and regular // operations defined on the interface, and is described in more detail // in section 4.5.4 below." assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); var desc = Object.getOwnPropertyDescriptor(self[this.name], "prototype"); assert_false("get" in desc, this.name + ".prototype should not have a getter"); assert_false("set" in desc, this.name + ".prototype should not have a setter"); assert_false(desc.writable, this.name + ".prototype should not be writable"); assert_false(desc.enumerable, this.name + ".prototype should not be enumerable"); assert_false(desc.configurable, this.name + ".prototype should not be configurable"); // Next, test that the [[Prototype]] of the interface prototype object // is correct. (This is made somewhat difficult by the existence of // [NoInterfaceObject].) // TODO: Aryeh thinks there's at least other place in this file where // we try to figure out if an interface prototype object is // correct. Consolidate that code. // "The interface prototype object for a given interface A must have an // internal [[Prototype]] property whose value is returned from the // following steps: // "If A is declared with the [Global] or [PrimaryGlobal] extended // attribute, and A supports named properties, then return the named // properties object for A, as defined in §3.6.4 Named properties // object. // "Otherwise, if A is declared to inherit from another interface, then // return the interface prototype object for the inherited interface. // "Otherwise, if A is declared with the [LegacyArrayClass] extended // attribute, then return %ArrayPrototype%. // "Otherwise, return %ObjectPrototype%. // // "In the ECMAScript binding, the DOMException type has some additional // requirements: // // "Unlike normal interface types, the interface prototype object // for DOMException must have as its [[Prototype]] the intrinsic // object %ErrorPrototype%." // if (this.name === "Window") { assert_class_string(Object.getPrototypeOf(self[this.name].prototype), 'WindowProperties', 'Class name for prototype of Window' + '.prototype is not "WindowProperties"'); } else { var inherit_interface, inherit_interface_has_interface_object; if (this.base) { inherit_interface = this.base; inherit_interface_has_interface_object = !this.array .members[inherit_interface] .has_extended_attribute("NoInterfaceObject"); } else if (this.has_extended_attribute('LegacyArrayClass')) { inherit_interface = 'Array'; inherit_interface_has_interface_object = true; } else if (this.name === "DOMException") { inherit_interface = 'Error'; inherit_interface_has_interface_object = true; } else { inherit_interface = 'Object'; inherit_interface_has_interface_object = true; } if (inherit_interface_has_interface_object) { assert_own_property(self, inherit_interface, 'should inherit from ' + inherit_interface + ', but self has no such property'); assert_own_property(self[inherit_interface], 'prototype', 'should inherit from ' + inherit_interface + ', but that object has no "prototype" property'); assert_equals(Object.getPrototypeOf(self[this.name].prototype), self[inherit_interface].prototype, 'prototype of ' + this.name + '.prototype is not ' + inherit_interface + '.prototype'); } else { // We can't test that we get the correct object, because this is the // only way to get our hands on it. We only test that its class // string, at least, is correct. assert_class_string(Object.getPrototypeOf(self[this.name].prototype), inherit_interface + 'Prototype', 'Class name for prototype of ' + this.name + '.prototype is not "' + inherit_interface + 'Prototype"'); } } // "The class string of an interface prototype object is the // concatenation of the interface’s identifier and the string // “Prototype”." // Skip these tests for now due to a specification issue about // prototype name. // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28244 // assert_class_string(self[this.name].prototype, this.name + "Prototype", // "class string of " + this.name + ".prototype"); // String() should end up calling {}.toString if nothing defines a // stringifier. if (!this.has_stringifier()) { // assert_equals(String(self[this.name].prototype), "[object " + this.name + "Prototype]", // "String(" + this.name + ".prototype)"); } }.bind(this), this.name + " interface: existence and properties of interface prototype object"); // "If the interface is declared with the [Global] or [PrimaryGlobal] // extended attribute, or the interface is in the set of inherited // interfaces for any other interface that is declared with one of these // attributes, then the interface prototype object must be an immutable // prototype exotic object." // https://heycam.github.io/webidl/#interface-prototype-object if (this.is_global()) { this.test_immutable_prototype("interface prototype object", self[this.name].prototype); } test(function() { if (this.is_callback() && !this.has_constants()) { return; } assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); if (this.is_callback()) { assert_false("prototype" in self[this.name], this.name + ' should not have a "prototype" property'); return; } assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); // "If the [NoInterfaceObject] extended attribute was not specified on // the interface, then the interface prototype object must also have a // property named “constructor” with attributes { [[Writable]]: true, // [[Enumerable]]: false, [[Configurable]]: true } whose value is a // reference to the interface object for the interface." assert_own_property(self[this.name].prototype, "constructor", this.name + '.prototype does not have own property "constructor"'); var desc = Object.getOwnPropertyDescriptor(self[this.name].prototype, "constructor"); assert_false("get" in desc, this.name + ".prototype.constructor should not have a getter"); assert_false("set" in desc, this.name + ".prototype.constructor should not have a setter"); assert_true(desc.writable, this.name + ".prototype.constructor should be writable"); assert_false(desc.enumerable, this.name + ".prototype.constructor should not be enumerable"); assert_true(desc.configurable, this.name + ".prototype.constructor should be configurable"); assert_equals(self[this.name].prototype.constructor, self[this.name], this.name + '.prototype.constructor is not the same object as ' + this.name); }.bind(this), this.name + ' interface: existence and properties of interface prototype object\'s "constructor" property'); }; //@} IdlInterface.prototype.test_immutable_prototype = function(type, obj) //@{ { if (typeof Object.setPrototypeOf !== "function") { return; } test(function(t) { var originalValue = Object.getPrototypeOf(obj); var newValue = Object.create(null); t.add_cleanup(function() { try { Object.setPrototypeOf(obj, originalValue); } catch (err) {} }); assert_throws(new TypeError(), function() { Object.setPrototypeOf(obj, newValue); }); assert_equals( Object.getPrototypeOf(obj), originalValue, "original value not modified" ); }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to a new value via Object.setPrototypeOf " + "should throw a TypeError"); test(function(t) { var originalValue = Object.getPrototypeOf(obj); var newValue = Object.create(null); t.add_cleanup(function() { var setter = Object.getOwnPropertyDescriptor( Object.prototype, '__proto__' ).set; try { setter.call(obj, originalValue); } catch (err) {} }); assert_throws(new TypeError(), function() { obj.__proto__ = newValue; }); assert_equals( Object.getPrototypeOf(obj), originalValue, "original value not modified" ); }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to a new value via __proto__ " + "should throw a TypeError"); test(function(t) { var originalValue = Object.getPrototypeOf(obj); var newValue = Object.create(null); t.add_cleanup(function() { try { Reflect.setPrototypeOf(obj, originalValue); } catch (err) {} }); assert_false(Reflect.setPrototypeOf(obj, newValue)); assert_equals( Object.getPrototypeOf(obj), originalValue, "original value not modified" ); }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to a new value via Reflect.setPrototypeOf " + "should return false"); test(function() { var originalValue = Object.getPrototypeOf(obj); Object.setPrototypeOf(obj, originalValue); }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to its original value via Object.setPrototypeOf " + "should not throw"); test(function() { var originalValue = Object.getPrototypeOf(obj); obj.__proto__ = originalValue; }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to its original value via __proto__ " + "should not throw"); test(function() { var originalValue = Object.getPrototypeOf(obj); assert_true(Reflect.setPrototypeOf(obj, originalValue)); }.bind(this), this.name + " interface: internal [[SetPrototypeOf]] method " + "of " + type + " - setting to its original value via Reflect.setPrototypeOf " + "should return true"); }; //@} IdlInterface.prototype.test_member_const = function(member) //@{ { if (!this.has_constants()) { throw "Internal error: test_member_const called without any constants"; } test(function() { assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); // "For each constant defined on an interface A, there must be // a corresponding property on the interface object, if it // exists." assert_own_property(self[this.name], member.name); // "The value of the property is that which is obtained by // converting the constant’s IDL value to an ECMAScript // value." assert_equals(self[this.name][member.name], constValue(member.value), "property has wrong value"); // "The property has attributes { [[Writable]]: false, // [[Enumerable]]: true, [[Configurable]]: false }." var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name); assert_false("get" in desc, "property should not have a getter"); assert_false("set" in desc, "property should not have a setter"); assert_false(desc.writable, "property should not be writable"); assert_true(desc.enumerable, "property should be enumerable"); assert_false(desc.configurable, "property should not be configurable"); }.bind(this), this.name + " interface: constant " + member.name + " on interface object"); // "In addition, a property with the same characteristics must // exist on the interface prototype object." test(function() { assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); if (this.is_callback()) { assert_false("prototype" in self[this.name], this.name + ' should not have a "prototype" property'); return; } assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); assert_own_property(self[this.name].prototype, member.name); assert_equals(self[this.name].prototype[member.name], constValue(member.value), "property has wrong value"); var desc = Object.getOwnPropertyDescriptor(self[this.name], member.name); assert_false("get" in desc, "property should not have a getter"); assert_false("set" in desc, "property should not have a setter"); assert_false(desc.writable, "property should not be writable"); assert_true(desc.enumerable, "property should be enumerable"); assert_false(desc.configurable, "property should not be configurable"); }.bind(this), this.name + " interface: constant " + member.name + " on interface prototype object"); }; //@} IdlInterface.prototype.test_member_attribute = function(member) //@{ { var a_test = async_test(this.name + " interface: attribute " + member.name); a_test.step(function() { if (this.is_callback() && !this.has_constants()) { a_test.done() return; } assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); if (member["static"]) { assert_own_property(self[this.name], member.name, "The interface object must have a property " + format_value(member.name)); a_test.done(); } else if (this.is_global()) { assert_own_property(self, member.name, "The global object must have a property " + format_value(member.name)); assert_false(member.name in self[this.name].prototype, "The prototype object should not have a property " + format_value(member.name)); var getter = Object.getOwnPropertyDescriptor(self, member.name).get; assert_equals(typeof(getter), "function", format_value(member.name) + " must have a getter"); // Try/catch around the get here, since it can legitimately throw. // If it does, we obviously can't check for equality with direct // invocation of the getter. var gotValue; var propVal; try { propVal = self[member.name]; gotValue = true; } catch (e) { gotValue = false; } if (gotValue) { assert_equals(propVal, getter.call(undefined), "Gets on a global should not require an explicit this"); } // do_interface_attribute_asserts must be the last thing we do, // since it will call done() on a_test. this.do_interface_attribute_asserts(self, member, a_test); } else { assert_true(member.name in self[this.name].prototype, "The prototype object must have a property " + format_value(member.name)); if (!member.has_extended_attribute("LenientThis")) { if (member.idlType.generic !== "Promise") { assert_throws(new TypeError(), function() { self[this.name].prototype[member.name]; }.bind(this), "getting property on prototype object must throw TypeError"); // do_interface_attribute_asserts must be the last thing we // do, since it will call done() on a_test. this.do_interface_attribute_asserts(self[this.name].prototype, member, a_test); } else { promise_rejects(a_test, new TypeError(), self[this.name].prototype[member.name]) .then(function() { // do_interface_attribute_asserts must be the last // thing we do, since it will call done() on a_test. this.do_interface_attribute_asserts(self[this.name].prototype, member, a_test); }.bind(this)); } } else { assert_equals(self[this.name].prototype[member.name], undefined, "getting property on prototype object must return undefined"); // do_interface_attribute_asserts must be the last thing we do, // since it will call done() on a_test. this.do_interface_attribute_asserts(self[this.name].prototype, member, a_test); } } }.bind(this)); }; //@} IdlInterface.prototype.test_member_operation = function(member) //@{ { var a_test = async_test(this.name + " interface: operation " + member.name + "(" + member.arguments.map( function(m) {return m.idlType.idlType; } ).join(", ") +")"); a_test.step(function() { // This function tests WebIDL as of 2015-12-29. // https://heycam.github.io/webidl/#es-operations if (this.is_callback() && !this.has_constants()) { a_test.done(); return; } assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); if (this.is_callback()) { assert_false("prototype" in self[this.name], this.name + ' should not have a "prototype" property'); a_test.done(); return; } assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); // "For each unique identifier of an exposed operation defined on the // interface, there must exist a corresponding property, unless the // effective overload set for that identifier and operation and with an // argument count of 0 has no entries." // TODO: Consider [Exposed]. // "The location of the property is determined as follows:" var memberHolderObject; // "* If the operation is static, then the property exists on the // interface object." if (member["static"]) { assert_own_property(self[this.name], member.name, "interface object missing static operation"); memberHolderObject = self[this.name]; // "* Otherwise, [...] if the interface was declared with the [Global] // or [PrimaryGlobal] extended attribute, then the property exists // on every object that implements the interface." } else if (this.is_global()) { assert_own_property(self, member.name, "global object missing non-static operation"); memberHolderObject = self; // "* Otherwise, the property exists solely on the interface’s // interface prototype object." } else { assert_own_property(self[this.name].prototype, member.name, "interface prototype object missing non-static operation"); memberHolderObject = self[this.name].prototype; } this.do_member_operation_asserts(memberHolderObject, member, a_test); }.bind(this)); }; //@} IdlInterface.prototype.do_member_operation_asserts = function(memberHolderObject, member, a_test) //@{ { var done = a_test.done.bind(a_test); var operationUnforgeable = member.isUnforgeable; var desc = Object.getOwnPropertyDescriptor(memberHolderObject, member.name); // "The property has attributes { [[Writable]]: B, // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the // operation is unforgeable on the interface, and true otherwise". assert_false("get" in desc, "property should not have a getter"); assert_false("set" in desc, "property should not have a setter"); assert_equals(desc.writable, !operationUnforgeable, "property should be writable if and only if not unforgeable"); assert_true(desc.enumerable, "property should be enumerable"); assert_equals(desc.configurable, !operationUnforgeable, "property should be configurable if and only if not unforgeable"); // "The value of the property is a Function object whose // behavior is as follows . . ." assert_equals(typeof memberHolderObject[member.name], "function", "property must be a function"); // "The value of the Function object’s “length” property is // a Number determined as follows: // ". . . // "Return the length of the shortest argument list of the // entries in S." assert_equals(memberHolderObject[member.name].length, minOverloadLength(this.members.filter(function(m) { return m.type == "operation" && m.name == member.name; })), "property has wrong .length"); // Make some suitable arguments var args = member.arguments.map(function(arg) { return create_suitable_object(arg.idlType); }); // "Let O be a value determined as follows: // ". . . // "Otherwise, throw a TypeError." // This should be hit if the operation is not static, there is // no [ImplicitThis] attribute, and the this value is null. // // TODO: We currently ignore the [ImplicitThis] case. Except we manually // check for globals, since otherwise we'll invoke window.close(). And we // have to skip this test for anything that on the proto chain of "self", // since that does in fact have implicit-this behavior. if (!member["static"]) { var cb; if (!this.is_global() && memberHolderObject[member.name] != self[member.name]) { cb = awaitNCallbacks(2, done); throwOrReject(a_test, member, memberHolderObject[member.name], null, args, "calling operation with this = null didn't throw TypeError", cb); } else { cb = awaitNCallbacks(1, done); } // ". . . If O is not null and is also not a platform object // that implements interface I, throw a TypeError." // // TODO: Test a platform object that implements some other // interface. (Have to be sure to get inheritance right.) throwOrReject(a_test, member, memberHolderObject[member.name], {}, args, "calling operation with this = {} didn't throw TypeError", cb); } else { done(); } } //@} IdlInterface.prototype.add_iterable_members = function(member) //@{ { this.members.push(new IdlInterfaceMember( { type: "operation", name: "entries", idlType: "iterator", arguments: []})); this.members.push(new IdlInterfaceMember( { type: "operation", name: "keys", idlType: "iterator", arguments: []})); this.members.push(new IdlInterfaceMember( { type: "operation", name: "values", idlType: "iterator", arguments: []})); this.members.push(new IdlInterfaceMember( { type: "operation", name: "forEach", idlType: "void", arguments: [{ name: "callback", idlType: {idlType: "function"}}, { name: "thisValue", idlType: {idlType: "any"}, optional: true}]})); }; IdlInterface.prototype.test_to_json_operation = function(memberHolderObject, member) { if (member.has_extended_attribute("Default")) { var map = this.default_to_json_operation(); test(function() { var json = memberHolderObject.toJSON(); map.forEach(function(type, k) { assert_true(k in json, "property " + JSON.stringify(k) + " should be present in the output of " + this.name + ".prototype.toJSON()"); var descriptor = Object.getOwnPropertyDescriptor(json, k); assert_true(descriptor.writable, "property " + k + " should be writable"); assert_true(descriptor.configurable, "property " + k + " should be configurable"); assert_true(descriptor.enumerable, "property " + k + " should be enumerable"); this.array.assert_type_is(json[k], type); delete json[k]; }, this); for (var k in json) { assert_unreached("property " + JSON.stringify(k) + " should not be present in the output of " + this.name + ".prototype.toJSON()"); } }.bind(this), "Test default toJSON operation of " + this.name); } else { test(function() { assert_true(this.array.is_json_type(member.idlType), JSON.stringify(member.idlType) + " is not an appropriate return value for the toJSON operation of " + this.name); this.array.assert_type_is(memberHolderObject.toJSON(), member.idlType); }.bind(this), "Test toJSON operation of " + this.name); } }; //@} IdlInterface.prototype.test_member_iterable = function(member) //@{ { var interfaceName = this.name; var isPairIterator = member.idlType instanceof Array; test(function() { var descriptor = Object.getOwnPropertyDescriptor(self[interfaceName].prototype, Symbol.iterator); assert_true(descriptor.writable, "property should be writable"); assert_true(descriptor.configurable, "property should be configurable"); assert_false(descriptor.enumerable, "property should not be enumerable"); assert_equals(self[interfaceName].prototype[Symbol.iterator].name, isPairIterator ? "entries" : "values", "@@iterator function does not have the right name"); }, "Testing Symbol.iterator property of iterable interface " + interfaceName); if (isPairIterator) { test(function() { assert_equals(self[interfaceName].prototype[Symbol.iterator], self[interfaceName].prototype["entries"], "entries method is not the same as @@iterator"); }, "Testing pair iterable interface " + interfaceName); } else { test(function() { ["entries", "keys", "values", "forEach", Symbol.Iterator].forEach(function(property) { assert_equals(self[interfaceName].prototype[property], Array.prototype[property], property + " function is not the same as Array one"); }); }, "Testing value iterable interface " + interfaceName); } }; //@} IdlInterface.prototype.test_member_stringifier = function(member) //@{ { test(function() { if (this.is_callback() && !this.has_constants()) { return; } assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); if (this.is_callback()) { assert_false("prototype" in self[this.name], this.name + ' should not have a "prototype" property'); return; } assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); // ". . . the property exists on the interface prototype object." var interfacePrototypeObject = self[this.name].prototype; assert_own_property(self[this.name].prototype, "toString", "interface prototype object missing non-static operation"); var stringifierUnforgeable = member.isUnforgeable; var desc = Object.getOwnPropertyDescriptor(interfacePrototypeObject, "toString"); // "The property has attributes { [[Writable]]: B, // [[Enumerable]]: true, [[Configurable]]: B }, where B is false if the // stringifier is unforgeable on the interface, and true otherwise." assert_false("get" in desc, "property should not have a getter"); assert_false("set" in desc, "property should not have a setter"); assert_equals(desc.writable, !stringifierUnforgeable, "property should be writable if and only if not unforgeable"); assert_true(desc.enumerable, "property should be enumerable"); assert_equals(desc.configurable, !stringifierUnforgeable, "property should be configurable if and only if not unforgeable"); // "The value of the property is a Function object, which behaves as // follows . . ." assert_equals(typeof interfacePrototypeObject.toString, "function", "property must be a function"); // "The value of the Function object’s “length” property is the Number // value 0." assert_equals(interfacePrototypeObject.toString.length, 0, "property has wrong .length"); // "Let O be the result of calling ToObject on the this value." assert_throws(new TypeError(), function() { self[this.name].prototype.toString.apply(null, []); }, "calling stringifier with this = null didn't throw TypeError"); // "If O is not an object that implements the interface on which the // stringifier was declared, then throw a TypeError." // // TODO: Test a platform object that implements some other // interface. (Have to be sure to get inheritance right.) assert_throws(new TypeError(), function() { self[this.name].prototype.toString.apply({}, []); }, "calling stringifier with this = {} didn't throw TypeError"); }.bind(this), this.name + " interface: stringifier"); }; //@} IdlInterface.prototype.test_members = function() //@{ { for (var i = 0; i < this.members.length; i++) { var member = this.members[i]; switch (member.type) { case "iterable": this.add_iterable_members(member); break; // TODO: add setlike and maplike handling. default: break; } } for (var i = 0; i < this.members.length; i++) { var member = this.members[i]; if (member.untested) { continue; } if (!exposed_in(exposure_set(member, this.exposureSet))) { test(function() { // It's not exposed, so we shouldn't find it anywhere. assert_false(member.name in self[this.name], "The interface object must not have a property " + format_value(member.name)); assert_false(member.name in self[this.name].prototype, "The prototype object must not have a property " + format_value(member.name)); }.bind(this), this.name + " interface: member " + member.name); continue; } switch (member.type) { case "const": this.test_member_const(member); break; case "attribute": // For unforgeable attributes, we do the checks in // test_interface_of instead. if (!member.isUnforgeable) { this.test_member_attribute(member); } if (member.stringifier) { this.test_member_stringifier(member); } break; case "operation": // TODO: Need to correctly handle multiple operations with the same // identifier. // For unforgeable operations, we do the checks in // test_interface_of instead. if (member.name) { if (!member.isUnforgeable) { this.test_member_operation(member); } } else if (member.stringifier) { this.test_member_stringifier(member); } break; case "iterable": this.test_member_iterable(member); break; default: // TODO: check more member types. break; } } }; //@} IdlInterface.prototype.test_object = function(desc) //@{ { var obj, exception = null; try { obj = eval(desc); } catch(e) { exception = e; } var expected_typeof = this.members.some(function(member) { return member.legacycaller; }) ? "function" : "object"; this.test_primary_interface_of(desc, obj, exception, expected_typeof); var current_interface = this; while (current_interface) { if (!(current_interface.name in this.array.members)) { throw "Interface " + current_interface.name + " not found (inherited by " + this.name + ")"; } if (current_interface.prevent_multiple_testing && current_interface.already_tested) { return; } current_interface.test_interface_of(desc, obj, exception, expected_typeof); current_interface = this.array.members[current_interface.base]; } }; //@} IdlInterface.prototype.test_primary_interface_of = function(desc, obj, exception, expected_typeof) //@{ { // Only the object itself, not its members, are tested here, so if the // interface is untested, there is nothing to do. if (this.untested) { return; } // "The internal [[SetPrototypeOf]] method of every platform object that // implements an interface with the [Global] or [PrimaryGlobal] extended // attribute must execute the same algorithm as is defined for the // [[SetPrototypeOf]] internal method of an immutable prototype exotic // object." // https://heycam.github.io/webidl/#platform-object-setprototypeof if (this.is_global()) { this.test_immutable_prototype("global platform object", obj); } // We can't easily test that its prototype is correct if there's no // interface object, or the object is from a different global environment // (not instanceof Object). TODO: test in this case that its prototype at // least looks correct, even if we can't test that it's actually correct. if (!this.has_extended_attribute("NoInterfaceObject") && (typeof obj != expected_typeof || obj instanceof Object)) { test(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); assert_own_property(self, this.name, "self does not have own property " + format_value(this.name)); assert_own_property(self[this.name], "prototype", 'interface "' + this.name + '" does not have own property "prototype"'); // "The value of the internal [[Prototype]] property of the // platform object is the interface prototype object of the primary // interface from the platform object’s associated global // environment." assert_equals(Object.getPrototypeOf(obj), self[this.name].prototype, desc + "'s prototype is not " + this.name + ".prototype"); }.bind(this), this.name + " must be primary interface of " + desc); } // "The class string of a platform object that implements one or more // interfaces must be the identifier of the primary interface of the // platform object." test(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); assert_class_string(obj, this.name, "class string of " + desc); if (!this.has_stringifier()) { assert_equals(String(obj), "[object " + this.name + "]", "String(" + desc + ")"); } }.bind(this), "Stringification of " + desc); }; //@} IdlInterface.prototype.test_interface_of = function(desc, obj, exception, expected_typeof) //@{ { // TODO: Indexed and named properties, more checks on interface members this.already_tested = true; for (var i = 0; i < this.members.length; i++) { var member = this.members[i]; if (member.untested) { continue; } if (!exposed_in(exposure_set(member, this.exposureSet))) { test(function() { assert_false(member.name in obj); }.bind(this), this.name + "interface: " + desc + 'must not have property "' + member.name + '"'); continue; } if (member.type == "attribute" && member.isUnforgeable) { var a_test = async_test(this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); a_test.step(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); // Call do_interface_attribute_asserts last, since it will call a_test.done() this.do_interface_attribute_asserts(obj, member, a_test); }.bind(this)); } else if (member.type == "operation" && member.name && member.isUnforgeable) { var a_test = async_test(this.name + " interface: " + desc + ' must have own property "' + member.name + '"'); a_test.step(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); assert_own_property(obj, member.name, "Doesn't have the unforgeable operation property"); this.do_member_operation_asserts(obj, member, a_test); }.bind(this)); } else if ((member.type == "const" || member.type == "attribute" || member.type == "operation") && member.name) { var described_name = member.name; if (member.type == "operation") { described_name += "(" + member.arguments.map(arg => arg.idlType.idlType).join(", ") + ")"; } test(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); if (!member["static"]) { if (!this.is_global()) { assert_inherits(obj, member.name); } else { assert_own_property(obj, member.name); } if (member.type == "const") { assert_equals(obj[member.name], constValue(member.value)); } if (member.type == "attribute") { // Attributes are accessor properties, so they might // legitimately throw an exception rather than returning // anything. var property, thrown = false; try { property = obj[member.name]; } catch (e) { thrown = true; } if (!thrown) { this.array.assert_type_is(property, member.idlType); } } if (member.type == "operation") { assert_equals(typeof obj[member.name], "function"); } } }.bind(this), this.name + " interface: " + desc + ' must inherit property "' + described_name + '" with the proper type'); } // TODO: This is wrong if there are multiple operations with the same // identifier. // TODO: Test passing arguments of the wrong type. if (member.type == "operation" && member.name && member.arguments.length) { var a_test = async_test( this.name + " interface: calling " + member.name + "(" + member.arguments.map(function(m) { return m.idlType.idlType; }).join(", ") + ") on " + desc + " with too few arguments must throw TypeError"); a_test.step(function() { assert_equals(exception, null, "Unexpected exception when evaluating object"); assert_equals(typeof obj, expected_typeof, "wrong typeof object"); var fn; if (!member["static"]) { if (!this.is_global() && !member.isUnforgeable) { assert_inherits(obj, member.name); } else { assert_own_property(obj, member.name); } fn = obj[member.name]; } else { assert_own_property(obj.constructor, member.name, "interface object must have static operation as own property"); fn = obj.constructor[member.name]; } var minLength = minOverloadLength(this.members.filter(function(m) { return m.type == "operation" && m.name == member.name; })); var args = []; var cb = awaitNCallbacks(minLength, a_test.done.bind(a_test)); for (var i = 0; i < minLength; i++) { throwOrReject(a_test, member, fn, obj, args, "Called with " + i + " arguments", cb); args.push(create_suitable_object(member.arguments[i].idlType)); } if (minLength === 0) { cb(); } }.bind(this)); } if (member.is_to_json_regular_operation()) { this.test_to_json_operation(obj, member); } } }; //@} IdlInterface.prototype.has_stringifier = function() //@{ { if (this.name === "DOMException") { // toString is inherited from Error, so don't assume we have the // default stringifer return true; } if (this.members.some(function(member) { return member.stringifier; })) { return true; } if (this.base && this.array.members[this.base].has_stringifier()) { return true; } return false; }; //@} IdlInterface.prototype.do_interface_attribute_asserts = function(obj, member, a_test) //@{ { // This function tests WebIDL as of 2015-01-27. // TODO: Consider [Exposed]. // This is called by test_member_attribute() with the prototype as obj if // it is not a global, and the global otherwise, and by test_interface_of() // with the object as obj. var pendingPromises = []; // "For each exposed attribute of the interface, whether it was declared on // the interface itself or one of its consequential interfaces, there MUST // exist a corresponding property. The characteristics of this property are // as follows:" // "The name of the property is the identifier of the attribute." assert_own_property(obj, member.name); // "The property has attributes { [[Get]]: G, [[Set]]: S, [[Enumerable]]: // true, [[Configurable]]: configurable }, where: // "configurable is false if the attribute was declared with the // [Unforgeable] extended attribute and true otherwise; // "G is the attribute getter, defined below; and // "S is the attribute setter, also defined below." var desc = Object.getOwnPropertyDescriptor(obj, member.name); assert_false("value" in desc, 'property descriptor should not have a "value" field'); assert_false("writable" in desc, 'property descriptor should not have a "writable" field'); assert_true(desc.enumerable, "property should be enumerable"); if (member.isUnforgeable) { assert_false(desc.configurable, "[Unforgeable] property must not be configurable"); } else { assert_true(desc.configurable, "property must be configurable"); } // "The attribute getter is a Function object whose behavior when invoked // is as follows:" assert_equals(typeof desc.get, "function", "getter must be Function"); // "If the attribute is a regular attribute, then:" if (!member["static"]) { // "If O is not a platform object that implements I, then: // "If the attribute was specified with the [LenientThis] extended // attribute, then return undefined. // "Otherwise, throw a TypeError." if (!member.has_extended_attribute("LenientThis")) { if (member.idlType.generic !== "Promise") { assert_throws(new TypeError(), function() { desc.get.call({}); }.bind(this), "calling getter on wrong object type must throw TypeError"); } else { pendingPromises.push( promise_rejects(a_test, new TypeError(), desc.get.call({}), "calling getter on wrong object type must reject the return promise with TypeError")); } } else { assert_equals(desc.get.call({}), undefined, "calling getter on wrong object type must return undefined"); } } // "The value of the Function object’s “length” property is the Number // value 0." assert_equals(desc.get.length, 0, "getter length must be 0"); // TODO: Test calling setter on the interface prototype (should throw // TypeError in most cases). if (member.readonly && !member.has_extended_attribute("LenientSetter") && !member.has_extended_attribute("PutForwards") && !member.has_extended_attribute("Replaceable")) { // "The attribute setter is undefined if the attribute is declared // readonly and has neither a [PutForwards] nor a [Replaceable] // extended attribute declared on it." assert_equals(desc.set, undefined, "setter must be undefined for readonly attributes"); } else { // "Otherwise, it is a Function object whose behavior when // invoked is as follows:" assert_equals(typeof desc.set, "function", "setter must be function for PutForwards, Replaceable, or non-readonly attributes"); // "If the attribute is a regular attribute, then:" if (!member["static"]) { // "If /validThis/ is false and the attribute was not specified // with the [LenientThis] extended attribute, then throw a // TypeError." // "If the attribute is declared with a [Replaceable] extended // attribute, then: ..." // "If validThis is false, then return." if (!member.has_extended_attribute("LenientThis")) { assert_throws(new TypeError(), function() { desc.set.call({}); }.bind(this), "calling setter on wrong object type must throw TypeError"); } else { assert_equals(desc.set.call({}), undefined, "calling setter on wrong object type must return undefined"); } } // "The value of the Function object’s “length” property is the Number // value 1." assert_equals(desc.set.length, 1, "setter length must be 1"); } Promise.all(pendingPromises).then(a_test.done.bind(a_test)); } //@} /// IdlInterfaceMember /// function IdlInterfaceMember(obj) //@{ { /** * obj is an object produced by the WebIDLParser.js "ifMember" production. * We just forward all properties to this object without modification, * except for special extAttrs handling. */ for (var k in obj) { this[k] = obj[k]; } if (!("extAttrs" in this)) { this.extAttrs = []; } this.isUnforgeable = this.has_extended_attribute("Unforgeable"); } //@} IdlInterfaceMember.prototype = Object.create(IdlObject.prototype); IdlInterfaceMember.prototype.is_to_json_regular_operation = function() { return this.type == "operation" && !this.static && this.name == "toJSON"; }; /// Internal helper functions /// function create_suitable_object(type) //@{ { /** * type is an object produced by the WebIDLParser.js "type" production. We * return a JavaScript value that matches the type, if we can figure out * how. */ if (type.nullable) { return null; } switch (type.idlType) { case "any": case "boolean": return true; case "byte": case "octet": case "short": case "unsigned short": case "long": case "unsigned long": case "long long": case "unsigned long long": case "float": case "double": case "unrestricted float": case "unrestricted double": return 7; case "DOMString": case "ByteString": case "USVString": return "foo"; case "object": return {a: "b"}; case "Node": return document.createTextNode("abc"); } return null; } //@} /// IdlEnum /// // Used for IdlArray.prototype.assert_type_is function IdlEnum(obj) //@{ { /** * obj is an object produced by the WebIDLParser.js "dictionary" * production. */ /** Self-explanatory. */ this.name = obj.name; /** An array of values produced by the "enum" production. */ this.values = obj.values; } //@} IdlEnum.prototype = Object.create(IdlObject.prototype); /// IdlTypedef /// // Used for IdlArray.prototype.assert_type_is function IdlTypedef(obj) //@{ { /** * obj is an object produced by the WebIDLParser.js "typedef" * production. */ /** Self-explanatory. */ this.name = obj.name; /** The idlType that we are supposed to be typedeffing to. */ this.idlType = obj.idlType; } //@} IdlTypedef.prototype = Object.create(IdlObject.prototype); }()); // vim: set expandtab shiftwidth=4 tabstop=4 foldmarker=@{,@} foldmethod=marker: jsonld.js-4.0.1/tests/webidl/testharness.js000066400000000000000000003142571401135163200207410ustar00rootroot00000000000000/*global self*/ /*jshint latedef: nofunc*/ /* Distributed under both the W3C Test Suite License [1] and the W3C 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the policies and contribution forms [3]. [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license [3] http://www.w3.org/2004/10/27-testcases */ /* Documentation: http://web-platform-tests.org/writing-tests/testharness-api.html * (../docs/_writing-tests/testharness-api.md) */ (function () { var debug = false; // default timeout is 10 seconds, test can override if needed var settings = { output:false, harness_timeout:{ "normal":10000, "long":60000 }, test_timeout:null, message_events: ["start", "test_state", "result", "completion"] }; var xhtml_ns = "http://www.w3.org/1999/xhtml"; /* * TestEnvironment is an abstraction for the environment in which the test * harness is used. Each implementation of a test environment has to provide * the following interface: * * interface TestEnvironment { * // Invoked after the global 'tests' object has been created and it's * // safe to call add_*_callback() to register event handlers. * void on_tests_ready(); * * // Invoked after setup() has been called to notify the test environment * // of changes to the test harness properties. * void on_new_harness_properties(object properties); * * // Should return a new unique default test name. * DOMString next_default_test_name(); * * // Should return the test harness timeout duration in milliseconds. * float test_timeout(); * * // Should return the global scope object. * object global_scope(); * }; */ /* * A test environment with a DOM. The global object is 'window'. By default * test results are displayed in a table. Any parent windows receive * callbacks or messages via postMessage() when test events occur. See * apisample11.html and apisample12.html. */ function WindowTestEnvironment() { this.name_counter = 0; this.window_cache = null; this.output_handler = null; this.all_loaded = false; var this_obj = this; this.message_events = []; this.dispatched_messages = []; this.message_functions = { start: [add_start_callback, remove_start_callback, function (properties) { this_obj._dispatch("start_callback", [properties], {type: "start", properties: properties}); }], test_state: [add_test_state_callback, remove_test_state_callback, function(test) { this_obj._dispatch("test_state_callback", [test], {type: "test_state", test: test.structured_clone()}); }], result: [add_result_callback, remove_result_callback, function (test) { this_obj.output_handler.show_status(); this_obj._dispatch("result_callback", [test], {type: "result", test: test.structured_clone()}); }], completion: [add_completion_callback, remove_completion_callback, function (tests, harness_status) { var cloned_tests = map(tests, function(test) { return test.structured_clone(); }); this_obj._dispatch("completion_callback", [tests, harness_status], {type: "complete", tests: cloned_tests, status: harness_status.structured_clone()}); }] } on_event(window, 'load', function() { this_obj.all_loaded = true; }); on_event(window, 'message', function(event) { if (event.data && event.data.type === "getmessages" && event.source) { // A window can post "getmessages" to receive a duplicate of every // message posted by this environment so far. This allows subscribers // from fetch_tests_from_window to 'catch up' to the current state of // this environment. for (var i = 0; i < this_obj.dispatched_messages.length; ++i) { event.source.postMessage(this_obj.dispatched_messages[i], "*"); } } }); } WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { this.dispatched_messages.push(message_arg); this._forEach_windows( function(w, same_origin) { if (same_origin) { try { var has_selector = selector in w; } catch(e) { // If document.domain was set at some point same_origin can be // wrong and the above will fail. has_selector = false; } if (has_selector) { try { w[selector].apply(undefined, callback_args); } catch (e) { if (debug) { throw e; } } } } if (supports_post_message(w) && w !== self) { w.postMessage(message_arg, "*"); } }); }; WindowTestEnvironment.prototype._forEach_windows = function(callback) { // Iterate of the the windows [self ... top, opener]. The callback is passed // two objects, the first one is the windows object itself, the second one // is a boolean indicating whether or not its on the same origin as the // current window. var cache = this.window_cache; if (!cache) { cache = [[self, true]]; var w = self; var i = 0; var so; while (w != w.parent) { w = w.parent; so = is_same_origin(w); cache.push([w, so]); i++; } w = window.opener; if (w) { cache.push([w, is_same_origin(w)]); } this.window_cache = cache; } forEach(cache, function(a) { callback.apply(null, a); }); }; WindowTestEnvironment.prototype.on_tests_ready = function() { var output = new Output(); this.output_handler = output; var this_obj = this; add_start_callback(function (properties) { this_obj.output_handler.init(properties); }); add_test_state_callback(function(test) { this_obj.output_handler.show_status(); }); add_result_callback(function (test) { this_obj.output_handler.show_status(); }); add_completion_callback(function (tests, harness_status) { this_obj.output_handler.show_results(tests, harness_status); }); this.setup_messages(settings.message_events); }; WindowTestEnvironment.prototype.setup_messages = function(new_events) { var this_obj = this; forEach(settings.message_events, function(x) { var current_dispatch = this_obj.message_events.indexOf(x) !== -1; var new_dispatch = new_events.indexOf(x) !== -1; if (!current_dispatch && new_dispatch) { this_obj.message_functions[x][0](this_obj.message_functions[x][2]); } else if (current_dispatch && !new_dispatch) { this_obj.message_functions[x][1](this_obj.message_functions[x][2]); } }); this.message_events = new_events; } WindowTestEnvironment.prototype.next_default_test_name = function() { //Don't use document.title to work around an Opera bug in XHTML documents var title = document.getElementsByTagName("title")[0]; var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; this.name_counter++; return prefix + suffix; }; WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { this.output_handler.setup(properties); if (properties.hasOwnProperty("message_events")) { this.setup_messages(properties.message_events); } }; WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { on_event(window, 'load', callback); }; WindowTestEnvironment.prototype.test_timeout = function() { var metas = document.getElementsByTagName("meta"); for (var i = 0; i < metas.length; i++) { if (metas[i].name == "timeout") { if (metas[i].content == "long") { return settings.harness_timeout.long; } break; } } return settings.harness_timeout.normal; }; WindowTestEnvironment.prototype.global_scope = function() { return window; }; /* * Base TestEnvironment implementation for a generic web worker. * * Workers accumulate test results. One or more clients can connect and * retrieve results from a worker at any time. * * WorkerTestEnvironment supports communicating with a client via a * MessagePort. The mechanism for determining the appropriate MessagePort * for communicating with a client depends on the type of worker and is * implemented by the various specializations of WorkerTestEnvironment * below. * * A client document using testharness can use fetch_tests_from_worker() to * retrieve results from a worker. See apisample16.html. */ function WorkerTestEnvironment() { this.name_counter = 0; this.all_loaded = true; this.message_list = []; this.message_ports = []; } WorkerTestEnvironment.prototype._dispatch = function(message) { this.message_list.push(message); for (var i = 0; i < this.message_ports.length; ++i) { this.message_ports[i].postMessage(message); } }; // The only requirement is that port has a postMessage() method. It doesn't // have to be an instance of a MessagePort, and often isn't. WorkerTestEnvironment.prototype._add_message_port = function(port) { this.message_ports.push(port); for (var i = 0; i < this.message_list.length; ++i) { port.postMessage(this.message_list[i]); } }; WorkerTestEnvironment.prototype.next_default_test_name = function() { var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; this.name_counter++; return "Untitled" + suffix; }; WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; WorkerTestEnvironment.prototype.on_tests_ready = function() { var this_obj = this; add_start_callback( function(properties) { this_obj._dispatch({ type: "start", properties: properties, }); }); add_test_state_callback( function(test) { this_obj._dispatch({ type: "test_state", test: test.structured_clone() }); }); add_result_callback( function(test) { this_obj._dispatch({ type: "result", test: test.structured_clone() }); }); add_completion_callback( function(tests, harness_status) { this_obj._dispatch({ type: "complete", tests: map(tests, function(test) { return test.structured_clone(); }), status: harness_status.structured_clone() }); }); }; WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; WorkerTestEnvironment.prototype.test_timeout = function() { // Tests running in a worker don't have a default timeout. I.e. all // worker tests behave as if settings.explicit_timeout is true. return null; }; WorkerTestEnvironment.prototype.global_scope = function() { return self; }; /* * Dedicated web workers. * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope * * This class is used as the test_environment when testharness is running * inside a dedicated worker. */ function DedicatedWorkerTestEnvironment() { WorkerTestEnvironment.call(this); // self is an instance of DedicatedWorkerGlobalScope which exposes // a postMessage() method for communicating via the message channel // established when the worker is created. this._add_message_port(self); } DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { WorkerTestEnvironment.prototype.on_tests_ready.call(this); // In the absence of an onload notification, we a require dedicated // workers to explicitly signal when the tests are done. tests.wait_for_finish = true; }; /* * Shared web workers. * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope * * This class is used as the test_environment when testharness is running * inside a shared web worker. */ function SharedWorkerTestEnvironment() { WorkerTestEnvironment.call(this); var this_obj = this; // Shared workers receive message ports via the 'onconnect' event for // each connection. self.addEventListener("connect", function(message_event) { this_obj._add_message_port(message_event.source); }, false); } SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { WorkerTestEnvironment.prototype.on_tests_ready.call(this); // In the absence of an onload notification, we a require shared // workers to explicitly signal when the tests are done. tests.wait_for_finish = true; }; /* * Service workers. * http://www.w3.org/TR/service-workers/ * * This class is used as the test_environment when testharness is running * inside a service worker. */ function ServiceWorkerTestEnvironment() { WorkerTestEnvironment.call(this); this.all_loaded = false; this.on_loaded_callback = null; var this_obj = this; self.addEventListener("message", function(event) { if (event.data && event.data.type && event.data.type === "connect") { if (event.ports && event.ports[0]) { // If a MessageChannel was passed, then use it to // send results back to the main window. This // allows the tests to work even if the browser // does not fully support MessageEvent.source in // ServiceWorkers yet. this_obj._add_message_port(event.ports[0]); event.ports[0].start(); } else { // If there is no MessageChannel, then attempt to // use the MessageEvent.source to send results // back to the main window. this_obj._add_message_port(event.source); } } }, false); // The oninstall event is received after the service worker script and // all imported scripts have been fetched and executed. It's the // equivalent of an onload event for a document. All tests should have // been added by the time this event is received, thus it's not // necessary to wait until the onactivate event. However, tests for // installed service workers need another event which is equivalent to // the onload event because oninstall is fired only on installation. The // onmessage event is used for that purpose since tests using // testharness.js should ask the result to its service worker by // PostMessage. If the onmessage event is triggered on the service // worker's context, that means the worker's script has been evaluated. on_event(self, "install", on_all_loaded); on_event(self, "message", on_all_loaded); function on_all_loaded() { if (this_obj.all_loaded) return; this_obj.all_loaded = true; if (this_obj.on_loaded_callback) { this_obj.on_loaded_callback(); } } } ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { if (this.all_loaded) { callback(); } else { this.on_loaded_callback = callback; } }; function create_test_environment() { if ('document' in self) { return new WindowTestEnvironment(); } if ('DedicatedWorkerGlobalScope' in self && self instanceof DedicatedWorkerGlobalScope) { return new DedicatedWorkerTestEnvironment(); } if ('SharedWorkerGlobalScope' in self && self instanceof SharedWorkerGlobalScope) { return new SharedWorkerTestEnvironment(); } if ('ServiceWorkerGlobalScope' in self && self instanceof ServiceWorkerGlobalScope) { return new ServiceWorkerTestEnvironment(); } if ('WorkerGlobalScope' in self && self instanceof WorkerGlobalScope) { return new DedicatedWorkerTestEnvironment(); } throw new Error("Unsupported test environment"); } var test_environment = create_test_environment(); function is_shared_worker(worker) { return 'SharedWorker' in self && worker instanceof SharedWorker; } function is_service_worker(worker) { // The worker object may be from another execution context, // so do not use instanceof here. return 'ServiceWorker' in self && Object.prototype.toString.call(worker) == '[object ServiceWorker]'; } /* * API functions */ function test(func, name, properties) { var test_name = name ? name : test_environment.next_default_test_name(); properties = properties ? properties : {}; var test_obj = new Test(test_name, properties); test_obj.step(func, test_obj, test_obj); if (test_obj.phase === test_obj.phases.STARTED) { test_obj.done(); } } function async_test(func, name, properties) { if (typeof func !== "function") { properties = name; name = func; func = null; } var test_name = name ? name : test_environment.next_default_test_name(); properties = properties ? properties : {}; var test_obj = new Test(test_name, properties); if (func) { test_obj.step(func, test_obj, test_obj); } return test_obj; } function promise_test(func, name, properties) { var test = async_test(name, properties); // If there is no promise tests queue make one. if (!tests.promise_tests) { tests.promise_tests = Promise.resolve(); } tests.promise_tests = tests.promise_tests.then(function() { var donePromise = new Promise(function(resolve) { test._add_cleanup(resolve); }); var promise = test.step(func, test, test); test.step(function() { assert_not_equals(promise, undefined); }); Promise.resolve(promise).then( function() { test.done(); }) .catch(test.step_func( function(value) { if (value instanceof AssertionError) { throw value; } assert(false, "promise_test", null, "Unhandled rejection with value: ${value}", {value:value}); })); return donePromise; }); } function promise_rejects(test, expected, promise, description) { return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) { assert_throws(expected, function() { throw e }, description); }); } /** * This constructor helper allows DOM events to be handled using Promises, * which can make it a lot easier to test a very specific series of events, * including ensuring that unexpected events are not fired at any point. */ function EventWatcher(test, watchedNode, eventTypes) { if (typeof eventTypes == 'string') { eventTypes = [eventTypes]; } var waitingFor = null; // This is null unless we are recording all events, in which case it // will be an Array object. var recordedEvents = null; var eventHandler = test.step_func(function(evt) { assert_true(!!waitingFor, 'Not expecting event, but got ' + evt.type + ' event'); assert_equals(evt.type, waitingFor.types[0], 'Expected ' + waitingFor.types[0] + ' event, but got ' + evt.type + ' event instead'); if (Array.isArray(recordedEvents)) { recordedEvents.push(evt); } if (waitingFor.types.length > 1) { // Pop first event from array waitingFor.types.shift(); return; } // We need to null out waitingFor before calling the resolve function // since the Promise's resolve handlers may call wait_for() which will // need to set waitingFor. var resolveFunc = waitingFor.resolve; waitingFor = null; // Likewise, we should reset the state of recordedEvents. var result = recordedEvents || evt; recordedEvents = null; resolveFunc(result); }); for (var i = 0; i < eventTypes.length; i++) { watchedNode.addEventListener(eventTypes[i], eventHandler, false); } /** * Returns a Promise that will resolve after the specified event or * series of events has occured. * * @param options An optional options object. If the 'record' property * on this object has the value 'all', when the Promise * returned by this function is resolved, *all* Event * objects that were waited for will be returned as an * array. * * For example, * * ```js * const watcher = new EventWatcher(t, div, [ 'animationstart', * 'animationiteration', * 'animationend' ]); * return watcher.wait_for([ 'animationstart', 'animationend' ], * { record: 'all' }).then(evts => { * assert_equals(evts[0].elapsedTime, 0.0); * assert_equals(evts[1].elapsedTime, 2.0); * }); * ``` */ this.wait_for = function(types, options) { if (waitingFor) { return Promise.reject('Already waiting for an event or events'); } if (typeof types == 'string') { types = [types]; } if (options && options.record && options.record === 'all') { recordedEvents = []; } return new Promise(function(resolve, reject) { waitingFor = { types: types, resolve: resolve, reject: reject }; }); }; function stop_watching() { for (var i = 0; i < eventTypes.length; i++) { watchedNode.removeEventListener(eventTypes[i], eventHandler, false); } }; test._add_cleanup(stop_watching); return this; } expose(EventWatcher, 'EventWatcher'); function setup(func_or_properties, maybe_properties) { var func = null; var properties = {}; if (arguments.length === 2) { func = func_or_properties; properties = maybe_properties; } else if (func_or_properties instanceof Function) { func = func_or_properties; } else { properties = func_or_properties; } tests.setup(func, properties); test_environment.on_new_harness_properties(properties); } function done() { if (tests.tests.length === 0) { tests.set_file_is_test(); } if (tests.file_is_test) { tests.tests[0].done(); } tests.end_wait(); } function generate_tests(func, args, properties) { forEach(args, function(x, i) { var name = x[0]; test(function() { func.apply(this, x.slice(1)); }, name, Array.isArray(properties) ? properties[i] : properties); }); } function on_event(object, event, callback) { object.addEventListener(event, callback, false); } function step_timeout(f, t) { var outer_this = this; var args = Array.prototype.slice.call(arguments, 2); return setTimeout(function() { f.apply(outer_this, args); }, t * tests.timeout_multiplier); } expose(test, 'test'); expose(async_test, 'async_test'); expose(promise_test, 'promise_test'); expose(promise_rejects, 'promise_rejects'); expose(generate_tests, 'generate_tests'); expose(setup, 'setup'); expose(done, 'done'); expose(on_event, 'on_event'); expose(step_timeout, 'step_timeout'); /* * Return a string truncated to the given length, with ... added at the end * if it was longer. */ function truncate(s, len) { if (s.length > len) { return s.substring(0, len - 3) + "..."; } return s; } /* * Return true if object is probably a Node object. */ function is_node(object) { // I use duck-typing instead of instanceof, because // instanceof doesn't work if the node is from another window (like an // iframe's contentWindow): // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 try { var has_node_properties = ("nodeType" in object && "nodeName" in object && "nodeValue" in object && "childNodes" in object); } catch (e) { // We're probably cross-origin, which means we aren't a node return false; } if (has_node_properties) { try { object.nodeType; } catch (e) { // The object is probably Node.prototype or another prototype // object that inherits from it, and not a Node instance. return false; } return true; } return false; } var replacements = { "0": "0", "1": "x01", "2": "x02", "3": "x03", "4": "x04", "5": "x05", "6": "x06", "7": "x07", "8": "b", "9": "t", "10": "n", "11": "v", "12": "f", "13": "r", "14": "x0e", "15": "x0f", "16": "x10", "17": "x11", "18": "x12", "19": "x13", "20": "x14", "21": "x15", "22": "x16", "23": "x17", "24": "x18", "25": "x19", "26": "x1a", "27": "x1b", "28": "x1c", "29": "x1d", "30": "x1e", "31": "x1f", "0xfffd": "ufffd", "0xfffe": "ufffe", "0xffff": "uffff", }; /* * Convert a value to a nice, human-readable string */ function format_value(val, seen) { if (!seen) { seen = []; } if (typeof val === "object" && val !== null) { if (seen.indexOf(val) >= 0) { return "[...]"; } seen.push(val); } if (Array.isArray(val)) { return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; } switch (typeof val) { case "string": val = val.replace("\\", "\\\\"); for (var p in replacements) { var replace = "\\" + replacements[p]; val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); } return '"' + val.replace(/"/g, '\\"') + '"'; case "boolean": case "undefined": return String(val); case "number": // In JavaScript, -0 === 0 and String(-0) == "0", so we have to // special-case. if (val === -0 && 1/val === -Infinity) { return "-0"; } return String(val); case "object": if (val === null) { return "null"; } // Special-case Node objects, since those come up a lot in my tests. I // ignore namespaces. if (is_node(val)) { switch (val.nodeType) { case Node.ELEMENT_NODE: var ret = "<" + val.localName; for (var i = 0; i < val.attributes.length; i++) { ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; } ret += ">" + val.innerHTML + ""; return "Element node " + truncate(ret, 60); case Node.TEXT_NODE: return 'Text node "' + truncate(val.data, 60) + '"'; case Node.PROCESSING_INSTRUCTION_NODE: return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); case Node.COMMENT_NODE: return "Comment node "; case Node.DOCUMENT_NODE: return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); case Node.DOCUMENT_TYPE_NODE: return "DocumentType node"; case Node.DOCUMENT_FRAGMENT_NODE: return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); default: return "Node object of unknown type"; } } /* falls through */ default: try { return typeof val + ' "' + truncate(String(val), 1000) + '"'; } catch(e) { return ("[stringifying object threw " + String(e) + " with type " + String(typeof e) + "]"); } } } expose(format_value, "format_value"); /* * Assertions */ function assert_true(actual, description) { assert(actual === true, "assert_true", description, "expected true got ${actual}", {actual:actual}); } expose(assert_true, "assert_true"); function assert_false(actual, description) { assert(actual === false, "assert_false", description, "expected false got ${actual}", {actual:actual}); } expose(assert_false, "assert_false"); function same_value(x, y) { if (y !== y) { //NaN case return x !== x; } if (x === 0 && y === 0) { //Distinguish +0 and -0 return 1/x === 1/y; } return x === y; } function assert_equals(actual, expected, description) { /* * Test if two primitives are equal or two objects * are the same object */ if (typeof actual != typeof expected) { assert(false, "assert_equals", description, "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", {expected:expected, actual:actual}); return; } assert(same_value(actual, expected), "assert_equals", description, "expected ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_equals, "assert_equals"); function assert_not_equals(actual, expected, description) { /* * Test if two primitives are unequal or two objects * are different objects */ assert(!same_value(actual, expected), "assert_not_equals", description, "got disallowed value ${actual}", {actual:actual}); } expose(assert_not_equals, "assert_not_equals"); function assert_in_array(actual, expected, description) { assert(expected.indexOf(actual) != -1, "assert_in_array", description, "value ${actual} not in array ${expected}", {actual:actual, expected:expected}); } expose(assert_in_array, "assert_in_array"); function assert_object_equals(actual, expected, description) { //This needs to be improved a great deal function check_equal(actual, expected, stack) { stack.push(actual); var p; for (p in actual) { assert(expected.hasOwnProperty(p), "assert_object_equals", description, "unexpected property ${p}", {p:p}); if (typeof actual[p] === "object" && actual[p] !== null) { if (stack.indexOf(actual[p]) === -1) { check_equal(actual[p], expected[p], stack); } } else { assert(same_value(actual[p], expected[p]), "assert_object_equals", description, "property ${p} expected ${expected} got ${actual}", {p:p, expected:expected, actual:actual}); } } for (p in expected) { assert(actual.hasOwnProperty(p), "assert_object_equals", description, "expected property ${p} missing", {p:p}); } stack.pop(); } check_equal(actual, expected, []); } expose(assert_object_equals, "assert_object_equals"); function assert_array_equals(actual, expected, description) { assert(actual.length === expected.length, "assert_array_equals", description, "lengths differ, expected ${expected} got ${actual}", {expected:expected.length, actual:actual.length}); for (var i = 0; i < actual.length; i++) { assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), "assert_array_equals", description, "property ${i}, property expected to be ${expected} but was ${actual}", {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", actual:actual.hasOwnProperty(i) ? "present" : "missing"}); assert(same_value(expected[i], actual[i]), "assert_array_equals", description, "property ${i}, expected ${expected} but got ${actual}", {i:i, expected:expected[i], actual:actual[i]}); } } expose(assert_array_equals, "assert_array_equals"); function assert_array_approx_equals(actual, expected, epsilon, description) { /* * Test if two primitive arrays are equal withing +/- epsilon */ assert(actual.length === expected.length, "assert_array_approx_equals", description, "lengths differ, expected ${expected} got ${actual}", {expected:expected.length, actual:actual.length}); for (var i = 0; i < actual.length; i++) { assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), "assert_array_approx_equals", description, "property ${i}, property expected to be ${expected} but was ${actual}", {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", actual:actual.hasOwnProperty(i) ? "present" : "missing"}); assert(typeof actual[i] === "number", "assert_array_approx_equals", description, "property ${i}, expected a number but got a ${type_actual}", {i:i, type_actual:typeof actual[i]}); assert(Math.abs(actual[i] - expected[i]) <= epsilon, "assert_array_approx_equals", description, "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}", {i:i, expected:expected[i], actual:actual[i]}); } } expose(assert_array_approx_equals, "assert_array_approx_equals"); function assert_approx_equals(actual, expected, epsilon, description) { /* * Test if two primitive numbers are equal withing +/- epsilon */ assert(typeof actual === "number", "assert_approx_equals", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(Math.abs(actual - expected) <= epsilon, "assert_approx_equals", description, "expected ${expected} +/- ${epsilon} but got ${actual}", {expected:expected, actual:actual, epsilon:epsilon}); } expose(assert_approx_equals, "assert_approx_equals"); function assert_less_than(actual, expected, description) { /* * Test if a primitive number is less than another */ assert(typeof actual === "number", "assert_less_than", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual < expected, "assert_less_than", description, "expected a number less than ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_less_than, "assert_less_than"); function assert_greater_than(actual, expected, description) { /* * Test if a primitive number is greater than another */ assert(typeof actual === "number", "assert_greater_than", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual > expected, "assert_greater_than", description, "expected a number greater than ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_greater_than, "assert_greater_than"); function assert_between_exclusive(actual, lower, upper, description) { /* * Test if a primitive number is between two others */ assert(typeof actual === "number", "assert_between_exclusive", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual > lower && actual < upper, "assert_between_exclusive", description, "expected a number greater than ${lower} " + "and less than ${upper} but got ${actual}", {lower:lower, upper:upper, actual:actual}); } expose(assert_between_exclusive, "assert_between_exclusive"); function assert_less_than_equal(actual, expected, description) { /* * Test if a primitive number is less than or equal to another */ assert(typeof actual === "number", "assert_less_than_equal", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual <= expected, "assert_less_than_equal", description, "expected a number less than or equal to ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_less_than_equal, "assert_less_than_equal"); function assert_greater_than_equal(actual, expected, description) { /* * Test if a primitive number is greater than or equal to another */ assert(typeof actual === "number", "assert_greater_than_equal", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual >= expected, "assert_greater_than_equal", description, "expected a number greater than or equal to ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_greater_than_equal, "assert_greater_than_equal"); function assert_between_inclusive(actual, lower, upper, description) { /* * Test if a primitive number is between to two others or equal to either of them */ assert(typeof actual === "number", "assert_between_inclusive", description, "expected a number but got a ${type_actual}", {type_actual:typeof actual}); assert(actual >= lower && actual <= upper, "assert_between_inclusive", description, "expected a number greater than or equal to ${lower} " + "and less than or equal to ${upper} but got ${actual}", {lower:lower, upper:upper, actual:actual}); } expose(assert_between_inclusive, "assert_between_inclusive"); function assert_regexp_match(actual, expected, description) { /* * Test if a string (actual) matches a regexp (expected) */ assert(expected.test(actual), "assert_regexp_match", description, "expected ${expected} but got ${actual}", {expected:expected, actual:actual}); } expose(assert_regexp_match, "assert_regexp_match"); function assert_class_string(object, class_string, description) { assert_equals({}.toString.call(object), "[object " + class_string + "]", description); } expose(assert_class_string, "assert_class_string"); function _assert_own_property(name) { return function(object, property_name, description) { assert(object.hasOwnProperty(property_name), name, description, "expected property ${p} missing", {p:property_name}); }; } expose(_assert_own_property("assert_exists"), "assert_exists"); expose(_assert_own_property("assert_own_property"), "assert_own_property"); function assert_not_exists(object, property_name, description) { assert(!object.hasOwnProperty(property_name), "assert_not_exists", description, "unexpected property ${p} found", {p:property_name}); } expose(assert_not_exists, "assert_not_exists"); function _assert_inherits(name) { return function (object, property_name, description) { assert(typeof object === "object" || typeof object === "function", name, description, "provided value is not an object"); assert("hasOwnProperty" in object, name, description, "provided value is an object but has no hasOwnProperty method"); assert(!object.hasOwnProperty(property_name), name, description, "property ${p} found on object expected in prototype chain", {p:property_name}); assert(property_name in object, name, description, "property ${p} not found in prototype chain", {p:property_name}); }; } expose(_assert_inherits("assert_inherits"), "assert_inherits"); expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); function assert_readonly(object, property_name, description) { var initial_value = object[property_name]; try { //Note that this can have side effects in the case where //the property has PutForwards object[property_name] = initial_value + "a"; //XXX use some other value here? assert(same_value(object[property_name], initial_value), "assert_readonly", description, "changing property ${p} succeeded", {p:property_name}); } finally { object[property_name] = initial_value; } } expose(assert_readonly, "assert_readonly"); function assert_throws(code, func, description) { try { func.call(this); assert(false, "assert_throws", description, "${func} did not throw", {func:func}); } catch (e) { if (e instanceof AssertionError) { throw e; } if (code === null) { throw new AssertionError('Test bug: need to pass exception to assert_throws()'); } if (typeof code === "object") { assert(typeof e == "object" && "name" in e && e.name == code.name, "assert_throws", description, "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", {func:func, actual:e, actual_name:e.name, expected:code, expected_name:code.name}); return; } var code_name_map = { INDEX_SIZE_ERR: 'IndexSizeError', HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', WRONG_DOCUMENT_ERR: 'WrongDocumentError', INVALID_CHARACTER_ERR: 'InvalidCharacterError', NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', NOT_FOUND_ERR: 'NotFoundError', NOT_SUPPORTED_ERR: 'NotSupportedError', INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', INVALID_STATE_ERR: 'InvalidStateError', SYNTAX_ERR: 'SyntaxError', INVALID_MODIFICATION_ERR: 'InvalidModificationError', NAMESPACE_ERR: 'NamespaceError', INVALID_ACCESS_ERR: 'InvalidAccessError', TYPE_MISMATCH_ERR: 'TypeMismatchError', SECURITY_ERR: 'SecurityError', NETWORK_ERR: 'NetworkError', ABORT_ERR: 'AbortError', URL_MISMATCH_ERR: 'URLMismatchError', QUOTA_EXCEEDED_ERR: 'QuotaExceededError', TIMEOUT_ERR: 'TimeoutError', INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', DATA_CLONE_ERR: 'DataCloneError' }; var name = code in code_name_map ? code_name_map[code] : code; var name_code_map = { IndexSizeError: 1, HierarchyRequestError: 3, WrongDocumentError: 4, InvalidCharacterError: 5, NoModificationAllowedError: 7, NotFoundError: 8, NotSupportedError: 9, InUseAttributeError: 10, InvalidStateError: 11, SyntaxError: 12, InvalidModificationError: 13, NamespaceError: 14, InvalidAccessError: 15, TypeMismatchError: 17, SecurityError: 18, NetworkError: 19, AbortError: 20, URLMismatchError: 21, QuotaExceededError: 22, TimeoutError: 23, InvalidNodeTypeError: 24, DataCloneError: 25, EncodingError: 0, NotReadableError: 0, UnknownError: 0, ConstraintError: 0, DataError: 0, TransactionInactiveError: 0, ReadOnlyError: 0, VersionError: 0, OperationError: 0, NotAllowedError: 0 }; if (!(name in name_code_map)) { throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); } var required_props = { code: name_code_map[name] }; if (required_props.code === 0 || (typeof e == "object" && "name" in e && e.name !== e.name.toUpperCase() && e.name !== "DOMException")) { // New style exception: also test the name property. required_props.name = name; } //We'd like to test that e instanceof the appropriate interface, //but we can't, because we don't know what window it was created //in. It might be an instanceof the appropriate interface on some //unknown other window. TODO: Work around this somehow? assert(typeof e == "object", "assert_throws", description, "${func} threw ${e} with type ${type}, not an object", {func:func, e:e, type:typeof e}); for (var prop in required_props) { assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], "assert_throws", description, "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); } } } expose(assert_throws, "assert_throws"); function assert_unreached(description) { assert(false, "assert_unreached", description, "Reached unreachable code"); } expose(assert_unreached, "assert_unreached"); function assert_any(assert_func, actual, expected_array) { var args = [].slice.call(arguments, 3); var errors = []; var passed = false; forEach(expected_array, function(expected) { try { assert_func.apply(this, [actual, expected].concat(args)); passed = true; } catch (e) { errors.push(e.message); } }); if (!passed) { throw new AssertionError(errors.join("\n\n")); } } expose(assert_any, "assert_any"); function Test(name, properties) { if (tests.file_is_test && tests.tests.length) { throw new Error("Tried to create a test with file_is_test"); } this.name = name; this.phase = tests.phase === tests.phases.ABORTED ? this.phases.COMPLETE : this.phases.INITIAL; this.status = this.NOTRUN; this.timeout_id = null; this.index = null; this.properties = properties; var timeout = properties.timeout ? properties.timeout : settings.test_timeout; if (timeout !== null) { this.timeout_length = timeout * tests.timeout_multiplier; } else { this.timeout_length = null; } this.message = null; this.stack = null; this.steps = []; this.cleanup_callbacks = []; this._user_defined_cleanup_count = 0; tests.push(this); } Test.statuses = { PASS:0, FAIL:1, TIMEOUT:2, NOTRUN:3 }; Test.prototype = merge({}, Test.statuses); Test.prototype.phases = { INITIAL:0, STARTED:1, HAS_RESULT:2, COMPLETE:3 }; Test.prototype.structured_clone = function() { if (!this._structured_clone) { var msg = this.message; msg = msg ? String(msg) : msg; this._structured_clone = merge({ name:String(this.name), properties:merge({}, this.properties), phases:merge({}, this.phases) }, Test.statuses); } this._structured_clone.status = this.status; this._structured_clone.message = this.message; this._structured_clone.stack = this.stack; this._structured_clone.index = this.index; this._structured_clone.phase = this.phase; return this._structured_clone; }; Test.prototype.step = function(func, this_obj) { if (this.phase > this.phases.STARTED) { return; } this.phase = this.phases.STARTED; //If we don't get a result before the harness times out that will be a test timout this.set_status(this.TIMEOUT, "Test timed out"); tests.started = true; tests.notify_test_state(this); if (this.timeout_id === null) { this.set_timeout(); } this.steps.push(func); if (arguments.length === 1) { this_obj = this; } try { return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); } catch (e) { if (this.phase >= this.phases.HAS_RESULT) { return; } var message = String((typeof e === "object" && e !== null) ? e.message : e); var stack = e.stack ? e.stack : null; this.set_status(this.FAIL, message, stack); this.phase = this.phases.HAS_RESULT; this.done(); } }; Test.prototype.step_func = function(func, this_obj) { var test_this = this; if (arguments.length === 1) { this_obj = test_this; } return function() { return test_this.step.apply(test_this, [func, this_obj].concat( Array.prototype.slice.call(arguments))); }; }; Test.prototype.step_func_done = function(func, this_obj) { var test_this = this; if (arguments.length === 1) { this_obj = test_this; } return function() { if (func) { test_this.step.apply(test_this, [func, this_obj].concat( Array.prototype.slice.call(arguments))); } test_this.done(); }; }; Test.prototype.unreached_func = function(description) { return this.step_func(function() { assert_unreached(description); }); }; Test.prototype.step_timeout = function(f, timeout) { var test_this = this; var args = Array.prototype.slice.call(arguments, 2); return setTimeout(this.step_func(function() { return f.apply(test_this, args); }), timeout * tests.timeout_multiplier); } /* * Private method for registering cleanup functions. `testharness.js` * internals should use this method instead of the public `add_cleanup` * method in order to hide implementation details from the harness status * message in the case errors. */ Test.prototype._add_cleanup = function(callback) { this.cleanup_callbacks.push(callback); }; /* * Schedule a function to be run after the test result is known, regardless * of passing or failing state. The behavior of this function will not * influence the result of the test, but if an exception is thrown, the * test harness will report an error. */ Test.prototype.add_cleanup = function(callback) { this._user_defined_cleanup_count += 1; this._add_cleanup(callback); }; Test.prototype.force_timeout = function() { this.set_status(this.TIMEOUT); this.phase = this.phases.HAS_RESULT; }; Test.prototype.set_timeout = function() { if (this.timeout_length !== null) { var this_obj = this; this.timeout_id = setTimeout(function() { this_obj.timeout(); }, this.timeout_length); } }; Test.prototype.set_status = function(status, message, stack) { this.status = status; this.message = message; this.stack = stack ? stack : null; }; Test.prototype.timeout = function() { this.timeout_id = null; this.set_status(this.TIMEOUT, "Test timed out"); this.phase = this.phases.HAS_RESULT; this.done(); }; Test.prototype.done = function() { if (this.phase == this.phases.COMPLETE) { return; } if (this.phase <= this.phases.STARTED) { this.set_status(this.PASS, null); } this.phase = this.phases.COMPLETE; clearTimeout(this.timeout_id); tests.result(this); this.cleanup(); }; /* * Invoke all specified cleanup functions. If one or more produce an error, * the context is in an unpredictable state, so all further testing should * be cancelled. */ Test.prototype.cleanup = function() { var error_count = 0; var total; forEach(this.cleanup_callbacks, function(cleanup_callback) { try { cleanup_callback(); } catch (e) { // Set test phase immediately so that tests declared // within subsequent cleanup functions are not run. tests.phase = tests.phases.ABORTED; error_count += 1; } }); if (error_count > 0) { total = this._user_defined_cleanup_count; tests.status.status = tests.status.ERROR; tests.status.message = "Test named '" + this.name + "' specified " + total + " 'cleanup' function" + (total > 1 ? "s" : "") + ", and " + error_count + " failed."; tests.status.stack = null; } }; /* * A RemoteTest object mirrors a Test object on a remote worker. The * associated RemoteWorker updates the RemoteTest object in response to * received events. In turn, the RemoteTest object replicates these events * on the local document. This allows listeners (test result reporting * etc..) to transparently handle local and remote events. */ function RemoteTest(clone) { var this_obj = this; Object.keys(clone).forEach( function(key) { this_obj[key] = clone[key]; }); this.index = null; this.phase = this.phases.INITIAL; this.update_state_from(clone); tests.push(this); } RemoteTest.prototype.structured_clone = function() { var clone = {}; Object.keys(this).forEach( (function(key) { var value = this[key]; if (typeof value === "object" && value !== null) { clone[key] = merge({}, value); } else { clone[key] = value; } }).bind(this)); clone.phases = merge({}, this.phases); return clone; }; RemoteTest.prototype.cleanup = function() {}; RemoteTest.prototype.phases = Test.prototype.phases; RemoteTest.prototype.update_state_from = function(clone) { this.status = clone.status; this.message = clone.message; this.stack = clone.stack; if (this.phase === this.phases.INITIAL) { this.phase = this.phases.STARTED; } }; RemoteTest.prototype.done = function() { this.phase = this.phases.COMPLETE; } /* * A RemoteContext listens for test events from a remote test context, such * as another window or a worker. These events are then used to construct * and maintain RemoteTest objects that mirror the tests running in the * remote context. * * An optional third parameter can be used as a predicate to filter incoming * MessageEvents. */ function RemoteContext(remote, message_target, message_filter) { this.running = true; this.tests = new Array(); var this_obj = this; remote.onerror = function(error) { this_obj.remote_error(error); }; // Keeping a reference to the remote object and the message handler until // remote_done() is seen prevents the remote object and its message channel // from going away before all the messages are dispatched. this.remote = remote; this.message_target = message_target; this.message_handler = function(message) { var passesFilter = !message_filter || message_filter(message); if (this_obj.running && message.data && passesFilter && (message.data.type in this_obj.message_handlers)) { this_obj.message_handlers[message.data.type].call(this_obj, message.data); } }; this.message_target.addEventListener("message", this.message_handler); } RemoteContext.prototype.remote_error = function(error) { var message = error.message || String(error); var filename = (error.filename ? " " + error.filename: ""); // FIXME: Display remote error states separately from main document // error state. this.remote_done({ status: { status: tests.status.ERROR, message: "Error in remote" + filename + ": " + message, stack: error.stack } }); if (error.preventDefault) { error.preventDefault(); } }; RemoteContext.prototype.test_state = function(data) { var remote_test = this.tests[data.test.index]; if (!remote_test) { remote_test = new RemoteTest(data.test); this.tests[data.test.index] = remote_test; } remote_test.update_state_from(data.test); tests.notify_test_state(remote_test); }; RemoteContext.prototype.test_done = function(data) { var remote_test = this.tests[data.test.index]; remote_test.update_state_from(data.test); remote_test.done(); tests.result(remote_test); }; RemoteContext.prototype.remote_done = function(data) { if (tests.status.status === null && data.status.status !== data.status.OK) { tests.status.status = data.status.status; tests.status.message = data.status.message; tests.status.stack = data.status.stack; } this.message_target.removeEventListener("message", this.message_handler); this.running = false; this.remote = null; this.message_target = null; if (tests.all_done()) { tests.complete(); } }; RemoteContext.prototype.message_handlers = { test_state: RemoteContext.prototype.test_state, result: RemoteContext.prototype.test_done, complete: RemoteContext.prototype.remote_done }; /* * Harness */ function TestsStatus() { this.status = null; this.message = null; this.stack = null; } TestsStatus.statuses = { OK:0, ERROR:1, TIMEOUT:2 }; TestsStatus.prototype = merge({}, TestsStatus.statuses); TestsStatus.prototype.structured_clone = function() { if (!this._structured_clone) { var msg = this.message; msg = msg ? String(msg) : msg; this._structured_clone = merge({ status:this.status, message:msg, stack:this.stack }, TestsStatus.statuses); } return this._structured_clone; }; function Tests() { this.tests = []; this.num_pending = 0; this.phases = { INITIAL:0, SETUP:1, HAVE_TESTS:2, HAVE_RESULTS:3, COMPLETE:4, ABORTED:5 }; this.phase = this.phases.INITIAL; this.properties = {}; this.wait_for_finish = false; this.processing_callbacks = false; this.allow_uncaught_exception = false; this.file_is_test = false; this.timeout_multiplier = 1; this.timeout_length = test_environment.test_timeout(); this.timeout_id = null; this.start_callbacks = []; this.test_state_callbacks = []; this.test_done_callbacks = []; this.all_done_callbacks = []; this.pending_remotes = []; this.status = new TestsStatus(); var this_obj = this; test_environment.add_on_loaded_callback(function() { if (this_obj.all_done()) { this_obj.complete(); } }); this.set_timeout(); } Tests.prototype.setup = function(func, properties) { if (this.phase >= this.phases.HAVE_RESULTS) { return; } if (this.phase < this.phases.SETUP) { this.phase = this.phases.SETUP; } this.properties = properties; for (var p in properties) { if (properties.hasOwnProperty(p)) { var value = properties[p]; if (p == "allow_uncaught_exception") { this.allow_uncaught_exception = value; } else if (p == "explicit_done" && value) { this.wait_for_finish = true; } else if (p == "explicit_timeout" && value) { this.timeout_length = null; if (this.timeout_id) { clearTimeout(this.timeout_id); } } else if (p == "timeout_multiplier") { this.timeout_multiplier = value; } } } if (func) { try { func(); } catch (e) { this.status.status = this.status.ERROR; this.status.message = String(e); this.status.stack = e.stack ? e.stack : null; } } this.set_timeout(); }; Tests.prototype.set_file_is_test = function() { if (this.tests.length > 0) { throw new Error("Tried to set file as test after creating a test"); } this.wait_for_finish = true; this.file_is_test = true; // Create the test, which will add it to the list of tests async_test(); }; Tests.prototype.set_timeout = function() { var this_obj = this; clearTimeout(this.timeout_id); if (this.timeout_length !== null) { this.timeout_id = setTimeout(function() { this_obj.timeout(); }, this.timeout_length); } }; Tests.prototype.timeout = function() { if (this.status.status === null) { this.status.status = this.status.TIMEOUT; } this.complete(); }; Tests.prototype.end_wait = function() { this.wait_for_finish = false; if (this.all_done()) { this.complete(); } }; Tests.prototype.push = function(test) { if (this.phase < this.phases.HAVE_TESTS) { this.start(); } this.num_pending++; test.index = this.tests.push(test); this.notify_test_state(test); }; Tests.prototype.notify_test_state = function(test) { var this_obj = this; forEach(this.test_state_callbacks, function(callback) { callback(test, this_obj); }); }; Tests.prototype.all_done = function() { return this.phase === this.phases.ABORTED || (this.tests.length > 0 && test_environment.all_loaded && this.num_pending === 0 && !this.wait_for_finish && !this.processing_callbacks && !this.pending_remotes.some(function(w) { return w.running; })); }; Tests.prototype.start = function() { this.phase = this.phases.HAVE_TESTS; this.notify_start(); }; Tests.prototype.notify_start = function() { var this_obj = this; forEach (this.start_callbacks, function(callback) { callback(this_obj.properties); }); }; Tests.prototype.result = function(test) { if (this.phase > this.phases.HAVE_RESULTS) { return; } this.phase = this.phases.HAVE_RESULTS; this.num_pending--; this.notify_result(test); }; Tests.prototype.notify_result = function(test) { var this_obj = this; this.processing_callbacks = true; forEach(this.test_done_callbacks, function(callback) { callback(test, this_obj); }); this.processing_callbacks = false; if (this_obj.all_done()) { this_obj.complete(); } }; Tests.prototype.complete = function() { if (this.phase === this.phases.COMPLETE) { return; } this.phase = this.phases.COMPLETE; var this_obj = this; this.tests.forEach( function(x) { if (x.phase < x.phases.COMPLETE) { this_obj.notify_result(x); x.cleanup(); x.phase = x.phases.COMPLETE; } } ); this.notify_complete(); }; /* * Determine if any tests share the same `name` property. Return an array * containing the names of any such duplicates. */ Tests.prototype.find_duplicates = function() { var names = Object.create(null); var duplicates = []; forEach (this.tests, function(test) { if (test.name in names && duplicates.indexOf(test.name) === -1) { duplicates.push(test.name); } names[test.name] = true; }); return duplicates; }; Tests.prototype.notify_complete = function() { var this_obj = this; var duplicates; if (this.status.status === null) { duplicates = this.find_duplicates(); // Test names are presumed to be unique within test files--this // allows consumers to use them for identification purposes. // Duplicated names violate this expectation and should therefore // be reported as an error. if (duplicates.length) { this.status.status = this.status.ERROR; this.status.message = duplicates.length + ' duplicate test name' + (duplicates.length > 1 ? 's' : '') + ': "' + duplicates.join('", "') + '"'; } else { this.status.status = this.status.OK; } } forEach (this.all_done_callbacks, function(callback) { callback(this_obj.tests, this_obj.status); }); }; /* * Constructs a RemoteContext that tracks tests from a specific worker. */ Tests.prototype.create_remote_worker = function(worker) { var message_port; if (is_service_worker(worker)) { // Microsoft Edge's implementation of ServiceWorker doesn't support MessagePort yet. // Feature detection isn't a straightforward option here; it's only possible in the // worker's script context. var isMicrosoftEdgeBrowser = navigator.userAgent.includes("Edge"); if (window.MessageChannel && !isMicrosoftEdgeBrowser) { // The ServiceWorker's implicit MessagePort is currently not // reliably accessible from the ServiceWorkerGlobalScope due to // Blink setting MessageEvent.source to null for messages sent // via ServiceWorker.postMessage(). Until that's resolved, // create an explicit MessageChannel and pass one end to the // worker. var message_channel = new MessageChannel(); message_port = message_channel.port1; message_port.start(); worker.postMessage({type: "connect"}, [message_channel.port2]); } else { // If MessageChannel is not available, then try the // ServiceWorker.postMessage() approach using MessageEvent.source // on the other end. message_port = navigator.serviceWorker; worker.postMessage({type: "connect"}); } } else if (is_shared_worker(worker)) { message_port = worker.port; message_port.start(); } else { message_port = worker; } return new RemoteContext(worker, message_port); }; /* * Constructs a RemoteContext that tracks tests from a specific window. */ Tests.prototype.create_remote_window = function(remote) { remote.postMessage({type: "getmessages"}, "*"); return new RemoteContext( remote, window, function(msg) { return msg.source === remote; } ); }; Tests.prototype.fetch_tests_from_worker = function(worker) { if (this.phase >= this.phases.COMPLETE) { return; } this.pending_remotes.push(this.create_remote_worker(worker)); }; function fetch_tests_from_worker(port) { tests.fetch_tests_from_worker(port); } expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); Tests.prototype.fetch_tests_from_window = function(remote) { if (this.phase >= this.phases.COMPLETE) { return; } this.pending_remotes.push(this.create_remote_window(remote)); }; function fetch_tests_from_window(window) { tests.fetch_tests_from_window(window); } expose(fetch_tests_from_window, 'fetch_tests_from_window'); function timeout() { if (tests.timeout_length === null) { tests.timeout(); } } expose(timeout, 'timeout'); function add_start_callback(callback) { tests.start_callbacks.push(callback); } function add_test_state_callback(callback) { tests.test_state_callbacks.push(callback); } function add_result_callback(callback) { tests.test_done_callbacks.push(callback); } function add_completion_callback(callback) { tests.all_done_callbacks.push(callback); } expose(add_start_callback, 'add_start_callback'); expose(add_test_state_callback, 'add_test_state_callback'); expose(add_result_callback, 'add_result_callback'); expose(add_completion_callback, 'add_completion_callback'); function remove(array, item) { var index = array.indexOf(item); if (index > -1) { array.splice(index, 1); } } function remove_start_callback(callback) { remove(tests.start_callbacks, callback); } function remove_test_state_callback(callback) { remove(tests.test_state_callbacks, callback); } function remove_result_callback(callback) { remove(tests.test_done_callbacks, callback); } function remove_completion_callback(callback) { remove(tests.all_done_callbacks, callback); } /* * Output listener */ function Output() { this.output_document = document; this.output_node = null; this.enabled = settings.output; this.phase = this.INITIAL; } Output.prototype.INITIAL = 0; Output.prototype.STARTED = 1; Output.prototype.HAVE_RESULTS = 2; Output.prototype.COMPLETE = 3; Output.prototype.setup = function(properties) { if (this.phase > this.INITIAL) { return; } //If output is disabled in testharnessreport.js the test shouldn't be //able to override that this.enabled = this.enabled && (properties.hasOwnProperty("output") ? properties.output : settings.output); }; Output.prototype.init = function(properties) { if (this.phase >= this.STARTED) { return; } if (properties.output_document) { this.output_document = properties.output_document; } else { this.output_document = document; } this.phase = this.STARTED; }; Output.prototype.resolve_log = function() { var output_document; if (typeof this.output_document === "function") { output_document = this.output_document.apply(undefined); } else { output_document = this.output_document; } if (!output_document) { return; } var node = output_document.getElementById("log"); if (!node) { if (!document.body || document.readyState == "loading") { return; } node = output_document.createElement("div"); node.id = "log"; output_document.body.appendChild(node); } this.output_document = output_document; this.output_node = node; }; Output.prototype.show_status = function() { if (this.phase < this.STARTED) { this.init(); } if (!this.enabled) { return; } if (this.phase < this.HAVE_RESULTS) { this.resolve_log(); this.phase = this.HAVE_RESULTS; } var done_count = tests.tests.length - tests.num_pending; if (this.output_node) { if (done_count < 100 || (done_count < 1000 && done_count % 100 === 0) || done_count % 1000 === 0) { this.output_node.textContent = "Running, " + done_count + " complete, " + tests.num_pending + " remain"; } } }; Output.prototype.show_results = function (tests, harness_status) { if (this.phase >= this.COMPLETE) { return; } if (!this.enabled) { return; } if (!this.output_node) { this.resolve_log(); } this.phase = this.COMPLETE; var log = this.output_node; if (!log) { return; } var output_document = this.output_document; while (log.lastChild) { log.removeChild(log.lastChild); } var harness_url = get_harness_url(); if (harness_url !== undefined) { var stylesheet = output_document.createElementNS(xhtml_ns, "link"); stylesheet.setAttribute("rel", "stylesheet"); stylesheet.setAttribute("href", harness_url + "testharness.css"); var heads = output_document.getElementsByTagName("head"); if (heads.length) { heads[0].appendChild(stylesheet); } } var status_text_harness = {}; status_text_harness[harness_status.OK] = "OK"; status_text_harness[harness_status.ERROR] = "Error"; status_text_harness[harness_status.TIMEOUT] = "Timeout"; var status_text = {}; status_text[Test.prototype.PASS] = "Pass"; status_text[Test.prototype.FAIL] = "Fail"; status_text[Test.prototype.TIMEOUT] = "Timeout"; status_text[Test.prototype.NOTRUN] = "Not Run"; var status_number = {}; forEach(tests, function(test) { var status = status_text[test.status]; if (status_number.hasOwnProperty(status)) { status_number[status] += 1; } else { status_number[status] = 1; } }); function status_class(status) { return status.replace(/\s/g, '').toLowerCase(); } var summary_template = ["section", {"id":"summary"}, ["h2", {}, "Summary"], function() { var status = status_text_harness[harness_status.status]; var rv = [["section", {}, ["p", {}, "Harness status: ", ["span", {"class":status_class(status)}, status ], ] ]]; if (harness_status.status === harness_status.ERROR) { rv[0].push(["pre", {}, harness_status.message]); if (harness_status.stack) { rv[0].push(["pre", {}, harness_status.stack]); } } return rv; }, ["p", {}, "Found ${num_tests} tests"], function() { var rv = [["div", {}]]; var i = 0; while (status_text.hasOwnProperty(i)) { if (status_number.hasOwnProperty(status_text[i])) { var status = status_text[i]; rv[0].push(["div", {"class":status_class(status)}, ["label", {}, ["input", {type:"checkbox", checked:"checked"}], status_number[status] + " " + status]]); } i++; } return rv; }, ]; log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); forEach(output_document.querySelectorAll("section#summary label"), function(element) { on_event(element, "click", function(e) { if (output_document.getElementById("results") === null) { e.preventDefault(); return; } var result_class = element.parentNode.getAttribute("class"); var style_element = output_document.querySelector("style#hide-" + result_class); var input_element = element.querySelector("input"); if (!style_element && !input_element.checked) { style_element = output_document.createElementNS(xhtml_ns, "style"); style_element.id = "hide-" + result_class; style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; output_document.body.appendChild(style_element); } else if (style_element && input_element.checked) { style_element.parentNode.removeChild(style_element); } }); }); // This use of innerHTML plus manual escaping is not recommended in // general, but is necessary here for performance. Using textContent // on each individual adds tens of seconds of execution time for // large test suites (tens of thousands of tests). function escape_html(s) { return s.replace(/\&/g, "&") .replace(/" + "ResultTest Name" + (assertions ? "Assertion" : "") + "Message" + ""; for (var i = 0; i < tests.length; i++) { html += '' + escape_html(status_text[tests[i].status]) + "" + escape_html(tests[i].name) + "" + (assertions ? escape_html(get_assertion(tests[i])) + "" : "") + escape_html(tests[i].message ? tests[i].message : " ") + (tests[i].stack ? "
" +
                 escape_html(tests[i].stack) +
                 "
": "") + ""; } html += ""; try { log.lastChild.innerHTML = html; } catch (e) { log.appendChild(document.createElementNS(xhtml_ns, "p")) .textContent = "Setting innerHTML for the log threw an exception."; log.appendChild(document.createElementNS(xhtml_ns, "pre")) .textContent = html; } }; /* * Template code * * A template is just a javascript structure. An element is represented as: * * [tag_name, {attr_name:attr_value}, child1, child2] * * the children can either be strings (which act like text nodes), other templates or * functions (see below) * * A text node is represented as * * ["{text}", value] * * String values have a simple substitution syntax; ${foo} represents a variable foo. * * It is possible to embed logic in templates by using a function in a place where a * node would usually go. The function must either return part of a template or null. * * In cases where a set of nodes are required as output rather than a single node * with children it is possible to just use a list * [node1, node2, node3] * * Usage: * * render(template, substitutions) - take a template and an object mapping * variable names to parameters and return either a DOM node or a list of DOM nodes * * substitute(template, substitutions) - take a template and variable mapping object, * make the variable substitutions and return the substituted template * */ function is_single_node(template) { return typeof template[0] === "string"; } function substitute(template, substitutions) { if (typeof template === "function") { var replacement = template(substitutions); if (!replacement) { return null; } return substitute(replacement, substitutions); } if (is_single_node(template)) { return substitute_single(template, substitutions); } return filter(map(template, function(x) { return substitute(x, substitutions); }), function(x) {return x !== null;}); } function substitute_single(template, substitutions) { var substitution_re = /\$\{([^ }]*)\}/g; function do_substitution(input) { var components = input.split(substitution_re); var rv = []; for (var i = 0; i < components.length; i += 2) { rv.push(components[i]); if (components[i + 1]) { rv.push(String(substitutions[components[i + 1]])); } } return rv; } function substitute_attrs(attrs, rv) { rv[1] = {}; for (var name in template[1]) { if (attrs.hasOwnProperty(name)) { var new_name = do_substitution(name).join(""); var new_value = do_substitution(attrs[name]).join(""); rv[1][new_name] = new_value; } } } function substitute_children(children, rv) { for (var i = 0; i < children.length; i++) { if (children[i] instanceof Object) { var replacement = substitute(children[i], substitutions); if (replacement !== null) { if (is_single_node(replacement)) { rv.push(replacement); } else { extend(rv, replacement); } } } else { extend(rv, do_substitution(String(children[i]))); } } return rv; } var rv = []; rv.push(do_substitution(String(template[0])).join("")); if (template[0] === "{text}") { substitute_children(template.slice(1), rv); } else { substitute_attrs(template[1], rv); substitute_children(template.slice(2), rv); } return rv; } function make_dom_single(template, doc) { var output_document = doc || document; var element; if (template[0] === "{text}") { element = output_document.createTextNode(""); for (var i = 1; i < template.length; i++) { element.data += template[i]; } } else { element = output_document.createElementNS(xhtml_ns, template[0]); for (var name in template[1]) { if (template[1].hasOwnProperty(name)) { element.setAttribute(name, template[1][name]); } } for (var i = 2; i < template.length; i++) { if (template[i] instanceof Object) { var sub_element = make_dom(template[i]); element.appendChild(sub_element); } else { var text_node = output_document.createTextNode(template[i]); element.appendChild(text_node); } } } return element; } function make_dom(template, substitutions, output_document) { if (is_single_node(template)) { return make_dom_single(template, output_document); } return map(template, function(x) { return make_dom_single(x, output_document); }); } function render(template, substitutions, output_document) { return make_dom(substitute(template, substitutions), output_document); } /* * Utility funcions */ function assert(expected_true, function_name, description, error, substitutions) { if (tests.tests.length === 0) { tests.set_file_is_test(); } if (expected_true !== true) { var msg = make_message(function_name, description, error, substitutions); throw new AssertionError(msg); } } function AssertionError(message) { this.message = message; this.stack = this.get_stack(); } expose(AssertionError, "AssertionError"); AssertionError.prototype = Object.create(Error.prototype); AssertionError.prototype.get_stack = function() { var stack = new Error().stack; // IE11 does not initialize 'Error.stack' until the object is thrown. if (!stack) { try { throw new Error(); } catch (e) { stack = e.stack; } } // 'Error.stack' is not supported in all browsers/versions if (!stack) { return "(Stack trace unavailable)"; } var lines = stack.split("\n"); // Create a pattern to match stack frames originating within testharness.js. These include the // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript // in case it contains RegExp characters. var script_url = get_script_url(); var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js"; var re = new RegExp(re_text + ":\\d+:\\d+"); // Some browsers include a preamble that specifies the type of the error object. Skip this by // advancing until we find the first stack frame originating from testharness.js. var i = 0; while (!re.test(lines[i]) && i < lines.length) { i++; } // Then skip the top frames originating from testharness.js to begin the stack at the test code. while (re.test(lines[i]) && i < lines.length) { i++; } // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. if (i >= lines.length) { return stack; } return lines.slice(i).join("\n"); } function make_message(function_name, description, error, substitutions) { for (var p in substitutions) { if (substitutions.hasOwnProperty(p)) { substitutions[p] = format_value(substitutions[p]); } } var node_form = substitute(["{text}", "${function_name}: ${description}" + error], merge({function_name:function_name, description:(description?description + " ":"")}, substitutions)); return node_form.slice(1).join(""); } function filter(array, callable, thisObj) { var rv = []; for (var i = 0; i < array.length; i++) { if (array.hasOwnProperty(i)) { var pass = callable.call(thisObj, array[i], i, array); if (pass) { rv.push(array[i]); } } } return rv; } function map(array, callable, thisObj) { var rv = []; rv.length = array.length; for (var i = 0; i < array.length; i++) { if (array.hasOwnProperty(i)) { rv[i] = callable.call(thisObj, array[i], i, array); } } return rv; } function extend(array, items) { Array.prototype.push.apply(array, items); } function forEach(array, callback, thisObj) { for (var i = 0; i < array.length; i++) { if (array.hasOwnProperty(i)) { callback.call(thisObj, array[i], i, array); } } } function merge(a,b) { var rv = {}; var p; for (p in a) { rv[p] = a[p]; } for (p in b) { rv[p] = b[p]; } return rv; } function expose(object, name) { var components = name.split("."); var target = test_environment.global_scope(); for (var i = 0; i < components.length - 1; i++) { if (!(components[i] in target)) { target[components[i]] = {}; } target = target[components[i]]; } target[components[components.length - 1]] = object; } function is_same_origin(w) { try { 'random_prop' in w; return true; } catch (e) { return false; } } /** Returns the 'src' URL of the first