diff --git a/src/BaseIRI.js b/src/BaseIRI.js index 8a410af0..c6c5273a 100644 --- a/src/BaseIRI.js +++ b/src/BaseIRI.js @@ -6,16 +6,18 @@ import { escapeRegex } from './Util'; const BASE_UNSUPPORTED = /^:?[^:?#]*(?:[?#]|$)|^file:|^[^:]*:\/*[^?#]+?\/(?:\.\.?(?:\/|$)|\/)/i; const SUFFIX_SUPPORTED = /^(?:(?:[^/?#]{3,}|\.?[^/?#.]\.?)(?:\/[^/?#]{3,}|\.?[^/?#.]\.?)*\/?)?(?:[?#]|$)/; const CURRENT = './'; +const ORIGIN = '/'; const PARENT = '../'; const QUERY = '?'; const FRAGMENT = '#'; export default class BaseIRI { - constructor(base) { + constructor(base, options = {}) { this.base = base; this._baseLength = 0; this._baseMatcher = null; this._pathReplacements = new Array(base.length + 1); + this._options = options; } static supports(base) { @@ -57,8 +59,14 @@ export default class BaseIRI { } // Precalculate parent path substitutions - for (let i = 0; i < segments.length; i++) - this._pathReplacements[segments[i]] = PARENT.repeat(segments.length - i - 1); + for (let i = 0; i < segments.length; i++) { + if (!this._options.absoluteIris || (3 * (segments.length - i - 1)) <= (segments[i] - segments[0])) { + this._pathReplacements[segments[i]] = PARENT.repeat(segments.length - i - 1); + } + else { + this._pathReplacements[segments[i]] = ORIGIN + this.base.slice(segments[0], segments[i]); + } + } this._pathReplacements[segments[segments.length - 1]] = CURRENT; // Add the remainder of the base IRI (without fragment) to the regex diff --git a/src/N3Writer.js b/src/N3Writer.js index 07f486be..3f4366a5 100644 --- a/src/N3Writer.js +++ b/src/N3Writer.js @@ -60,7 +60,7 @@ export default class N3Writer { this._prefixIRIs = Object.create(null); options.prefixes && this.addPrefixes(options.prefixes); if (options.baseIRI) { - this._baseIri = new BaseIRI(options.baseIRI); + this._baseIri = new BaseIRI(options.baseIRI, { absoluteIris: options.absoluteIris }); } } else { diff --git a/test/BaseIRI-test.js b/test/BaseIRI-test.js index 85f5bb08..562ee4ac 100644 --- a/test/BaseIRI-test.js +++ b/test/BaseIRI-test.js @@ -137,12 +137,30 @@ describe('BaseIRI', () => { relativizes('an IRI containing ../ in its fragment', 'http://example.org/foo/', 'http://example.org/foo/baz#bar/../baz', 'baz#bar/../baz'); + + relativizes('an IRI where it is better to use a / path', 'http://example.org/foo/baz/nook/task/tar/', + 'http://example.org/foo/bar', '../../../../bar'); + + relativizes('an IRI where it is better to use a / path', 'http://example.org/foo/baz/nook/task/tar/', + 'http://example.org/foo/bar', '/foo/bar', true); + + relativizes('an IRI where it is better to use a / path [/x/y]', 'http://example.org/x/q/r/n/m/', + 'http://example.org/x/y', '../../../../y'); + + relativizes('an IRI where it is better to use a / path [/x/y]', 'http://example.org/x/q/r/n/m/', + 'http://example.org/x/y', '/x/y', true); }); }); -function relativizes(description, base, absolute, relative) { - it(`${relative ? 'relativizes' : 'does not relativize'} ${description}`, () => { - const baseIri = new BaseIRI(base); +function relativizes(description, base, absolute, relative, absoluteIris = false) { + it(`${relative ? 'relativizes' : 'does not relativize'} ${description} [absoluteIris: ${absoluteIris}]`, () => { + const baseIri = new BaseIRI(base, { absoluteIris }); expect(baseIri.toRelative(absolute)).toBe(relative || absolute); }); + if (!absoluteIris) { + it(`${relative ? 'relativizes' : 'does not relativize'} ${description}`, () => { + const baseIri = new BaseIRI(base); + expect(baseIri.toRelative(absolute)).toBe(relative || absolute); + }); + } } diff --git a/test/N3Writer-test.js b/test/N3Writer-test.js index 39143146..30ef7d5c 100644 --- a/test/N3Writer-test.js +++ b/test/N3Writer-test.js @@ -5565,11 +5565,116 @@ describe('Writer', () => { { input: 'http://example.org/foo?query#fragment#extended', expected: '../../../foo?query#fragment#extended' }, ); + testRelativizes({ baseIRI: 'http://example.org/foo/bar/baz/?query#fragment', absoluteIris: true }, + { input: 'http://example.org/', expected: '/' }, + { input: 'http://example.org/?', expected: '/?' }, + { input: 'http://example.org/#', expected: '/#' }, + { input: 'http://example.org/?query', expected: '/?query' }, + { input: 'http://example.org/#fragment', expected: '/#fragment' }, + { input: 'http://example.org/?query#', expected: '/?query#' }, + { input: 'http://example.org/?query#fragment', expected: '/?query#fragment' }, + { input: 'http://example.org/foo', expected: '/foo' }, + { input: 'http://example.org/foo?', expected: '/foo?' }, + { input: 'http://example.org/foo#', expected: '/foo#' }, + { input: 'http://example.org/foo?query', expected: '/foo?query' }, + { input: 'http://example.org/foo#fragment', expected: '/foo#fragment' }, + { input: 'http://example.org/foo?query#', expected: '/foo?query#' }, + { input: 'http://example.org/foo?query#fragment', expected: '/foo?query#fragment' }, + { input: 'http://example.org/foo/', expected: '/foo/' }, + { input: 'http://example.org/foo/?', expected: '/foo/?' }, + { input: 'http://example.org/foo/#', expected: '/foo/#' }, + { input: 'http://example.org/foo/?query', expected: '/foo/?query' }, + { input: 'http://example.org/foo/#fragment', expected: '/foo/#fragment' }, + { input: 'http://example.org/foo/?query#', expected: '/foo/?query#' }, + { input: 'http://example.org/foo/?query#fragment', expected: '/foo/?query#fragment' }, + { input: 'http://example.org/foo/bar', expected: '/foo/bar' }, + { input: 'http://example.org/foo/bar?', expected: '/foo/bar?' }, + { input: 'http://example.org/foo/bar#', expected: '/foo/bar#' }, + { input: 'http://example.org/foo/bar?query', expected: '/foo/bar?query' }, + { input: 'http://example.org/foo/bar#fragment', expected: '/foo/bar#fragment' }, + { input: 'http://example.org/foo/bar?query#', expected: '/foo/bar?query#' }, + { input: 'http://example.org/foo/bar?query#fragment', expected: '/foo/bar?query#fragment' }, + { input: 'http://example.org/foo/bar/', expected: '../' }, + { input: 'http://example.org/foo/bar/?', expected: '../?' }, + { input: 'http://example.org/foo/bar/#', expected: '../#' }, + { input: 'http://example.org/foo/bar/?query', expected: '../?query' }, + { input: 'http://example.org/foo/bar/#fragment', expected: '../#fragment' }, + { input: 'http://example.org/foo/bar/?query#', expected: '../?query#' }, + { input: 'http://example.org/foo/bar/?query#fragment', expected: '../?query#fragment' }, + { input: 'http://example.org/extended', expected: '/extended' }, + { input: 'http://example.org/?extended', expected: '/?extended' }, + { input: 'http://example.org/#extended', expected: '/#extended' }, + { input: 'http://example.org/?queryextended', expected: '/?queryextended' }, + { input: 'http://example.org/#fragmentextended', expected: '/#fragmentextended' }, + { input: 'http://example.org/?query#extended', expected: '/?query#extended' }, + { input: 'http://example.org/?query#fragmentextended', expected: '/?query#fragmentextended' }, + { input: 'http://example.org/fooextended', expected: '/fooextended' }, + { input: 'http://example.org/foo?extended', expected: '/foo?extended' }, + { input: 'http://example.org/foo#extended', expected: '/foo#extended' }, + { input: 'http://example.org/foo?queryextended', expected: '/foo?queryextended' }, + { input: 'http://example.org/foo#fragmentextended', expected: '/foo#fragmentextended' }, + { input: 'http://example.org/foo?query#extended', expected: '/foo?query#extended' }, + { input: 'http://example.org/foo?query#fragmentextended', expected: '/foo?query#fragmentextended' }, + { input: 'http://example.org/foo/extended', expected: '/foo/extended' }, + { input: 'http://example.org/foo/?extended', expected: '/foo/?extended' }, + { input: 'http://example.org/foo/#extended', expected: '/foo/#extended' }, + { input: 'http://example.org/foo/?queryextended', expected: '/foo/?queryextended' }, + { input: 'http://example.org/foo/#fragmentextended', expected: '/foo/#fragmentextended' }, + { input: 'http://example.org/foo/?query#extended', expected: '/foo/?query#extended' }, + { input: 'http://example.org/foo/?query#fragmentextended', expected: '/foo/?query#fragmentextended' }, + { input: 'http://example.org/foo/barextended', expected: '/foo/barextended' }, + { input: 'http://example.org/foo/bar?extended', expected: '/foo/bar?extended' }, + { input: 'http://example.org/foo/bar#extended', expected: '/foo/bar#extended' }, + { input: 'http://example.org/foo/bar?queryextended', expected: '/foo/bar?queryextended' }, + { input: 'http://example.org/foo/bar#fragmentextended', expected: '/foo/bar#fragmentextended' }, + { input: 'http://example.org/foo/bar?query#extended', expected: '/foo/bar?query#extended' }, + { input: 'http://example.org/foo/bar?query#fragmentextended', expected: '/foo/bar?query#fragmentextended' }, + { input: 'http://example.org/foo/bar/extended', expected: '../extended' }, + { input: 'http://example.org/foo/bar/?extended', expected: '../?extended' }, + { input: 'http://example.org/foo/bar/#extended', expected: '../#extended' }, + { input: 'http://example.org/foo/bar/?queryextended', expected: '../?queryextended' }, + { input: 'http://example.org/foo/bar/#fragmentextended', expected: '../#fragmentextended' }, + { input: 'http://example.org/foo/bar/?query#extended', expected: '../?query#extended' }, + { input: 'http://example.org/foo/bar/?query#fragmentextended', expected: '../?query#fragmentextended' }, + { input: 'http://example.org/?/extended', expected: '/?/extended' }, + { input: 'http://example.org/??extended', expected: '/??extended' }, + { input: 'http://example.org/#/extended', expected: '/#/extended' }, + { input: 'http://example.org/#?extended', expected: '/#?extended' }, + { input: 'http://example.org/##extended', expected: '/##extended' }, + { input: 'http://example.org/?query/extended', expected: '/?query/extended' }, + { input: 'http://example.org/?query?extended', expected: '/?query?extended' }, + { input: 'http://example.org/#fragment/extended', expected: '/#fragment/extended' }, + { input: 'http://example.org/#fragment?extended', expected: '/#fragment?extended' }, + { input: 'http://example.org/#fragment#extended', expected: '/#fragment#extended' }, + { input: 'http://example.org/?query#fragment/extended', expected: '/?query#fragment/extended' }, + { input: 'http://example.org/?query#fragment?extended', expected: '/?query#fragment?extended' }, + { input: 'http://example.org/?query#fragment#extended', expected: '/?query#fragment#extended' }, + { input: 'http://example.org/foo?/extended', expected: '/foo?/extended' }, + { input: 'http://example.org/foo??extended', expected: '/foo??extended' }, + { input: 'http://example.org/foo#/extended', expected: '/foo#/extended' }, + { input: 'http://example.org/foo#?extended', expected: '/foo#?extended' }, + { input: 'http://example.org/foo##extended', expected: '/foo##extended' }, + { input: 'http://example.org/foo?query/extended', expected: '/foo?query/extended' }, + { input: 'http://example.org/foo?query?extended', expected: '/foo?query?extended' }, + { input: 'http://example.org/foo#fragment/extended', expected: '/foo#fragment/extended' }, + { input: 'http://example.org/foo#fragment?extended', expected: '/foo#fragment?extended' }, + { input: 'http://example.org/foo#fragment#extended', expected: '/foo#fragment#extended' }, + { input: 'http://example.org/foo?query#fragment/extended', expected: '/foo?query#fragment/extended' }, + { input: 'http://example.org/foo?query#fragment?extended', expected: '/foo?query#fragment?extended' }, + { input: 'http://example.org/foo?query#fragment#extended', expected: '/foo?query#fragment#extended' }, + ); + function testRelativizes(baseIRI, ...cases) { + let absoluteIris = false; + if (typeof baseIRI === 'object') { + absoluteIris = baseIRI.absoluteIris; + baseIRI = baseIRI.baseIRI; + } + describe(`baseIRI ${baseIRI}`, () => { const parser = new Parser({ baseIRI }); for (const { input, expected } of cases) { - const writer = new Writer({ baseIRI }); + const writer = new Writer({ baseIRI, absoluteIris }); const quad = new Quad(new NamedNode('urn:ex:s'), new NamedNode('urn:ex:p'), new NamedNode(input)); it(`relativizes <${input}> to <${expected}>`, async () => { writer.addQuad(quad);