diff --git a/src/IRIs.js b/src/IRIs.js index 774d8575..77822ab0 100644 --- a/src/IRIs.js +++ b/src/IRIs.js @@ -11,11 +11,12 @@ export default { string: `${XSD}string`, }, rdf: { - type: `${RDF}type`, - nil: `${RDF}nil`, - first: `${RDF}first`, - rest: `${RDF}rest`, - langString: `${RDF}langString`, + type: `${RDF}type`, + nil: `${RDF}nil`, + first: `${RDF}first`, + rest: `${RDF}rest`, + langString: `${RDF}langString`, + dirLangString: `${RDF}dirLangString`, }, owl: { sameAs: 'http://www.w3.org/2002/07/owl#sameAs', diff --git a/src/N3DataFactory.js b/src/N3DataFactory.js index a0624a99..c3eaf540 100644 --- a/src/N3DataFactory.js +++ b/src/N3DataFactory.js @@ -88,8 +88,17 @@ export class Literal extends Term { // Find the last quotation mark (e.g., '"abc"@en-us') const id = this.id; let atPos = id.lastIndexOf('"') + 1; + const dirPos = id.lastIndexOf('--'); // If "@" it follows, return the remaining substring; empty otherwise - return atPos < id.length && id[atPos++] === '@' ? id.substr(atPos).toLowerCase() : ''; + return atPos < id.length && id[atPos++] === '@' ? (dirPos > atPos ? id.substr(0, dirPos) : id).substr(atPos).toLowerCase() : ''; + } + + // ### The direction of this literal + get direction() { + // Find the last double dash (e.g., '"abc"@en-us--ltr') + const id = this.id; + const atPos = id.lastIndexOf('--') + 2; + return atPos > 1 && atPos < id.length ? id.substr(atPos).toLowerCase() : ''; } // ### The datatype IRI of this literal @@ -104,8 +113,8 @@ export class Literal extends Term { const char = dtPos < id.length ? id[dtPos] : ''; // If "^" it follows, return the remaining substring return char === '^' ? id.substr(dtPos + 2) : - // If "@" follows, return rdf:langString; xsd:string otherwise - (char !== '@' ? xsd.string : rdf.langString); + // If "@" follows, return rdf:langString or rdf:dirLangString; xsd:string otherwise + (char !== '@' ? xsd.string : (id.indexOf('--', dtPos) > 0 ? rdf.dirLangString : rdf.langString)); } // ### Returns whether this object represents the same term as the other @@ -119,14 +128,16 @@ export class Literal extends Term { this.termType === other.termType && this.value === other.value && this.language === other.language && + ((this.direction === other.direction) || (this.direction === '' && !other.direction)) && this.datatype.value === other.datatype.value; } toJSON() { return { - termType: this.termType, - value: this.value, - language: this.language, + termType: this.termType, + value: this.value, + language: this.language, + direction: this.direction, datatype: { termType: 'NamedNode', value: this.datatypeString }, }; } @@ -216,9 +227,22 @@ export function termFromId(id, factory, nested) { return factory.literal(id.substr(1, id.length - 2)); // Literal with datatype or language const endPos = id.lastIndexOf('"', id.length - 1); + let languageOrDatatype; + if (id[endPos + 1] === '@') { + languageOrDatatype = id.substr(endPos + 2); + const dashDashIndex = languageOrDatatype.lastIndexOf('--'); + if (dashDashIndex > 0 && dashDashIndex < languageOrDatatype.length) { + languageOrDatatype = { + language: languageOrDatatype.substr(0, dashDashIndex), + direction: languageOrDatatype.substr(dashDashIndex + 2), + }; + } + } + else { + languageOrDatatype = factory.namedNode(id.substr(endPos + 3)); + } return factory.literal(id.substr(1, endPos - 1), - id[endPos + 1] === '@' ? id.substr(endPos + 2) - : factory.namedNode(id.substr(endPos + 3))); + languageOrDatatype); case '[': id = JSON.parse(id); break; @@ -255,7 +279,7 @@ export function termToId(term, nested) { case 'Variable': return `?${term.value}`; case 'DefaultGraph': return ''; case 'Literal': return `"${term.value}"${ - term.language ? `@${term.language}` : + term.language ? `@${term.language}${term.direction ? `--${term.direction}` : ''}` : (term.datatype && term.datatype.value !== xsd.string ? `^^${term.datatype.value}` : '')}`; case 'Quad': const res = [ @@ -350,6 +374,11 @@ function literal(value, languageOrDataType) { if (typeof languageOrDataType === 'string') return new Literal(`"${value}"@${languageOrDataType.toLowerCase()}`); + // Create a language-tagged string with base direction + if (languageOrDataType !== undefined && !('termType' in languageOrDataType)) { + return new Literal(`"${value}"@${languageOrDataType.language.toLowerCase()}--${languageOrDataType.direction.toLowerCase()}`); + } + // Automatically determine datatype for booleans and numbers let datatype = languageOrDataType ? languageOrDataType.value : ''; if (datatype === '') { diff --git a/src/N3Lexer.js b/src/N3Lexer.js index 4e734a06..22af54a3 100644 --- a/src/N3Lexer.js +++ b/src/N3Lexer.js @@ -21,6 +21,7 @@ const lineModeRegExps = { _unescapedIri: true, _simpleQuotedString: true, _langcode: true, + _dircode: true, _blank: true, _newline: true, _comment: true, @@ -38,7 +39,8 @@ export default class N3Lexer { this._unescapedIri = /^<([^\x00-\x20<>\\"\{\}\|\^\`]*)>[ \t]*/; // IRI without escape sequences; no unescaping this._simpleQuotedString = /^"([^"\\\r\n]*)"(?=[^"])/; // string without escape sequences this._simpleApostropheString = /^'([^'\\\r\n]*)'(?=[^'])/; - this._langcode = /^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9\-])/i; + this._langcode = /^@([a-z]+(?:-[a-z0-9]+)*)(?=[^a-z0-9])/i; + this._dircode = /^--(ltr)|(rtl)/; this._prefix = /^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:(?=[#\s<])/; this._prefixed = /^((?:[A-Za-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:\.?[\-0-9A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)?:((?:(?:[0-:A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])(?:(?:[\.\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~])*(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff]|%[0-9a-fA-F]{2}|\\[!#-\/;=?\-@_~]))?)?)(?:[ \t]+|(?=\.?[,;!\^\s#()\[\]\{\}"'<>]))/; this._variable = /^\?(?:(?:[A-Z_a-z\xc0-\xd6\xd8-\xf6\xf8-\u02ff\u0370-\u037d\u037f-\u1fff\u200c\u200d\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])(?:[\-0-:A-Z_a-z\xb7\xc0-\xd6\xd8-\xf6\xf8-\u037d\u037f-\u1fff\u200c\u200d\u203f\u2040\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff\uf900-\ufdcf\ufdf0-\ufffd]|[\ud800-\udb7f][\udc00-\udfff])*)(?=[.,;!\^\s#()\[\]\{\}"'<>])/; @@ -240,6 +242,13 @@ export default class N3Lexer { case '9': case '+': case '-': + if (input[1] === '-') { + // Try to find a direction code + if (this._previousMarker === 'langcode' && (match = this._dircode.exec(input))) + type = 'dircode', matchLength = 2, value = (match[1] || match[2]), matchLength = value.length + 2; + break; + } + // Try to find a number. Since it can contain (but not end with) a dot, // we always need a non-dot character before deciding it is a number. // Therefore, try inserting a space if we're at the end of the input. diff --git a/src/N3Parser.js b/src/N3Parser.js index 33919405..378aae3a 100644 --- a/src/N3Parser.js +++ b/src/N3Parser.js @@ -522,9 +522,10 @@ export default class N3Parser { } // ### `_completeLiteral` completes a literal with an optional datatype or language - _completeLiteral(token) { + _completeLiteral(token, component) { // Create a simple string literal by default let literal = this._factory.literal(this._literalValue); + let readCb; switch (token.type) { // Create a datatyped literal @@ -538,37 +539,71 @@ export default class N3Parser { // Create a language-tagged string case 'langcode': literal = this._factory.literal(this._literalValue, token.value); + this._literalLanguage = token.value; token = null; + readCb = this._readDirCode.bind(this, component); break; } - return { token, literal }; + return { token, literal, readCb }; + } + + _readDirCode(component, listItem, token) { + // Attempt to read a dircode + if (token.type === 'dircode') { + const term = this._factory.literal(this._literalValue, { language: this._literalLanguage, direction: token.value }); + if (component === 'subject') + this._subject = term; + else + this._object = term; + this._literalLanguage = undefined; + token = null; + } + + if (component === 'subject') + return token === null ? this._readPredicateOrNamedGraph : this._readPredicateOrNamedGraph(token); + return this._completeObjectLiteralPost(token, listItem); } // Completes a literal in subject position _completeSubjectLiteral(token) { - this._subject = this._completeLiteral(token).literal; + const completed = this._completeLiteral(token, 'subject'); + this._subject = completed.literal; + + // Postpone completion if the literal is only partially completed (such as lang+dir). + if (completed.readCb) + return completed.readCb.bind(this, false); + return this._readPredicateOrNamedGraph; } // Completes a literal in object position _completeObjectLiteral(token, listItem) { - const completed = this._completeLiteral(token); + const completed = this._completeLiteral(token, 'object'); if (!completed) return; + this._object = completed.literal; + // Postpone completion if the literal is only partially completed (such as lang+dir). + if (completed.readCb) + return completed.readCb.bind(this, listItem); + + return this._completeObjectLiteralPost(completed.token, listItem); + } + + _completeObjectLiteralPost(token, listItem) { // If this literal was part of a list, write the item // (we could also check the context stack, but passing in a flag is faster) if (listItem) this._emit(this._subject, this.RDF_FIRST, this._object, this._graph); // If the token was consumed, continue with the rest of the input - if (completed.token === null) + if (token === null) return this._getContextEndReader(); // Otherwise, consume the token now else { this._readCallback = this._getContextEndReader(); - return this._readCallback(completed.token); + return this._readCallback(token); } } diff --git a/src/N3Writer.js b/src/N3Writer.js index 6eb16a33..6bd2cd1c 100644 --- a/src/N3Writer.js +++ b/src/N3Writer.js @@ -172,8 +172,9 @@ export default class N3Writer { value = value.replace(escapeAll, characterReplacer); // Write a language-tagged literal + const direction = literal.direction ? `--${literal.direction}` : ''; if (literal.language) - return `"${value}"@${literal.language}`; + return `"${value}"@${literal.language}${direction}`; // Write dedicated literals per data type if (this._lineMode) { diff --git a/test/Literal-test.js b/test/Literal-test.js index 9bcc4f4f..be2f6adb 100644 --- a/test/Literal-test.js +++ b/test/Literal-test.js @@ -39,6 +39,10 @@ describe('Literal', () => { expect(literal).toHaveProperty('language', ''); }); + it('should have the empty string as direction', () => { + expect(literal).toHaveProperty('direction', ''); + }); + it('should have xsd:string as datatype', () => { expect(literal).toHaveProperty('datatype'); expect(literal.datatype).toBeInstanceOf(NamedNode); @@ -123,6 +127,7 @@ describe('Literal', () => { termType: 'Literal', value: '', language: '', + direction: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/2001/XMLSchema#string', @@ -239,6 +244,7 @@ describe('Literal', () => { termType: 'Literal', value: 'my @^^ string', language: '', + direction: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/2001/XMLSchema#string', @@ -271,6 +277,10 @@ describe('Literal', () => { expect(literal).toHaveProperty('language', 'en-us'); }); + it('should have the empty string as direction', () => { + expect(literal).toHaveProperty('direction', ''); + }); + it('should have rdf:langString as datatype', () => { expect(literal).toHaveProperty('datatype'); expect(literal.datatype).toBeInstanceOf(NamedNode); @@ -355,6 +365,7 @@ describe('Literal', () => { termType: 'Literal', value: '', language: 'en-us', + direction: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString', @@ -363,6 +374,132 @@ describe('Literal', () => { }); }); + describe('A Literal instance created from the empty string with a language and direction', () => { + let literal; + beforeAll(() => { literal = new Literal('""@en-us--rtl'); }); + + it('should be a Literal', () => { + expect(literal).toBeInstanceOf(Literal); + }); + + it('should be a Term', () => { + expect(literal).toBeInstanceOf(Term); + }); + + it('should have term type "Literal"', () => { + expect(literal.termType).toBe('Literal'); + }); + + it('should have the empty string as value', () => { + expect(literal).toHaveProperty('value', ''); + }); + + it('should have the language tag as language', () => { + expect(literal).toHaveProperty('language', 'en-us'); + }); + + it('should have the direction as direction', () => { + expect(literal).toHaveProperty('direction', 'rtl'); + }); + + it('should have rdf:langString as datatype', () => { + expect(literal).toHaveProperty('datatype'); + expect(literal.datatype).toBeInstanceOf(NamedNode); + expect(literal.datatype.value).toBe('http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString'); + }); + + it('should have the quoted empty string as id', () => { + expect(literal).toHaveProperty('id', '""@en-us--rtl'); + }); + + it('should equal a Literal instance with the same value', () => { + expect(literal.equals(new Literal('""@en-us--rtl'))).toBe(true); + }); + + it( + 'should equal an object with the same term type, value, language, and datatype', + () => { + expect(literal.equals({ + termType: 'Literal', + value: '', + language: 'en-us', + direction: 'rtl', + datatype: { value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', termType: 'NamedNode' }, + })).toBe(true); + }, + ); + + it('should not equal a falsy object', () => { + expect(literal.equals(null)).toBe(false); + }); + + it('should not equal a Literal instance with a non-empty value', () => { + expect(literal.equals(new Literal('"x"'))).toBe(false); + }); + + it( + 'should not equal an object with the same term type but a different value', + () => { + expect(literal.equals({ + termType: 'Literal', + value: 'x', + language: 'en-us', + direction: 'rtl', + datatype: { value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', termType: 'NamedNode' }, + })).toBe(false); + }, + ); + + it( + 'should not equal an object with the same term type but a different language', + () => { + expect(literal.equals({ + termType: 'Literal', + value: '', + language: '', + direction: 'rtl', + datatype: { value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', termType: 'NamedNode' }, + })).toBe(false); + }, + ); + + it( + 'should not equal an object with the same term type but a different datatype', + () => { + expect(literal.equals({ + termType: 'Literal', + value: '', + language: 'en-us', + direction: 'rtl', + datatype: { value: 'other', termType: 'NamedNode' }, + })).toBe(false); + }, + ); + + it('should not equal an object with a different term type', () => { + expect(literal.equals({ + termType: 'NamedNode', + value: '', + language: 'en-us', + direction: 'rtl', + datatype: { value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString', termType: 'NamedNode' }, + })).toBe(false); + }); + + it('should provide a JSON representation', () => { + expect(literal.toJSON()).toEqual({ + termType: 'Literal', + value: '', + language: 'en-us', + direction: 'rtl', + datatype: { + termType: 'NamedNode', + value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', + }, + }); + }); + }); + describe('A Literal instance created from a string without language or datatype', () => { let literal; beforeAll(() => { literal = new Literal('"my @^^ string"@en-us'); }); @@ -387,6 +524,10 @@ describe('Literal', () => { expect(literal).toHaveProperty('language', 'en-us'); }); + it('should have the empty string as direction', () => { + expect(literal).toHaveProperty('direction', ''); + }); + it('should have rdf:langString as datatype', () => { expect(literal).toHaveProperty('datatype'); expect(literal.datatype).toBeInstanceOf(NamedNode); @@ -471,6 +612,7 @@ describe('Literal', () => { termType: 'Literal', value: 'my @^^ string', language: 'en-us', + direction: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString', @@ -503,6 +645,10 @@ describe('Literal', () => { expect(literal).toHaveProperty('language', ''); }); + it('should have the empty string as direction', () => { + expect(literal).toHaveProperty('direction', ''); + }); + it('should have the datatype', () => { expect(literal).toHaveProperty('datatype'); expect(literal.datatype).toBeInstanceOf(NamedNode); @@ -587,6 +733,7 @@ describe('Literal', () => { termType: 'Literal', value: '', language: '', + direction: '', datatype: { termType: 'NamedNode', value: 'http://example.org/types#type', @@ -621,6 +768,10 @@ describe('Literal', () => { expect(literal).toHaveProperty('language', ''); }); + it('should have the empty string as direction', () => { + expect(literal).toHaveProperty('direction', ''); + }); + it('should have the datatype', () => { expect(literal).toHaveProperty('datatype'); expect(literal.datatype).toBeInstanceOf(NamedNode); @@ -709,6 +860,7 @@ describe('Literal', () => { termType: 'Literal', value: 'my @^^ string', language: '', + direction: '', datatype: { termType: 'NamedNode', value: 'http://example.org/types#type', diff --git a/test/N3DataFactory-test.js b/test/N3DataFactory-test.js index e97dde60..920caffc 100644 --- a/test/N3DataFactory-test.js +++ b/test/N3DataFactory-test.js @@ -52,6 +52,10 @@ describe('DataFactory', () => { expect(DataFactory.literal('abc', 'en-GB')).toEqual(new Literal('"abc"@en-gb')); }); + it('converts a non-empty string with a language and direction', () => { + expect(DataFactory.literal('abc', { language: 'en-GB', direction: 'rtl' })).toEqual(new Literal('"abc"@en-gb--rtl')); + }); + it('converts a non-empty string with a named node type', () => { expect(DataFactory.literal('abc', new NamedNode('http://ex.org/type'))).toEqual(new Literal('"abc"^^http://ex.org/type')); }); diff --git a/test/N3Lexer-test.js b/test/N3Lexer-test.js index 3b4c4ed8..ab2d3ee0 100644 --- a/test/N3Lexer-test.js +++ b/test/N3Lexer-test.js @@ -414,6 +414,27 @@ describe('Lexer', () => { { type: 'eof', line: 1 }), ); + it( + 'should tokenize a quoted string literal with directional language code', + shouldTokenize('"string"@en--rtl "string"@nl-be--ltr "string"@EN--rtl ', + { type: 'literal', value: 'string', line: 1 }, + { type: 'langcode', value: 'en', line: 1 }, + { type: 'dircode', value: 'rtl', line: 1 }, + { type: 'literal', value: 'string', line: 1 }, + { type: 'langcode', value: 'nl-be', line: 1 }, + { type: 'dircode', value: 'ltr', line: 1 }, + { type: 'literal', value: 'string', line: 1 }, + { type: 'langcode', value: 'EN', line: 1 }, + { type: 'dircode', value: 'rtl', line: 1 }, + { type: 'eof', line: 1 }), + ); + + it('should not tokenize an invalid direction', shouldNotTokenize('"string"@en--unk', + 'Unexpected "--unk" on line 1.')); + + it('should not tokenize a direction in uppercase', shouldNotTokenize('"string"@en--LTR', + 'Unexpected "--LTR" on line 1.')); + it( 'should tokenize a quoted string literal with type', shouldTokenize('"stringA"^^ "stringB"^^ns:mytype ', diff --git a/test/N3Parser-test.js b/test/N3Parser-test.js index 961e8ad6..b11275bb 100644 --- a/test/N3Parser-test.js +++ b/test/N3Parser-test.js @@ -99,6 +99,28 @@ describe('Parser', () => { ['a', 'b', '"string"@en']), ); + it( + 'should parse a triple with a literal with directional language code', + shouldParse(' "string"@en--rtl.', + ['a', 'b', '"string"@en--rtl']), + ); + + it( + 'should error on a triple with a literal with direction but without language code', + shouldNotParse(' "string"--rtl.', + 'Unexpected "--rtl." on line 1.', { + line: 1, + previousToken: { + line: 1, + type: 'literal', + value: 'string', + prefix: '', + start: 8, + end: 16, + }, + }), + ); + it( 'should parse a triple with a literal and an IRI type', shouldParse(' "string"^^.', @@ -567,6 +589,14 @@ describe('Parser', () => { ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']), ); + it( + 'should parse a list with a directional language-tagged literal', + shouldParse(' ("x"@en-GB--ltr).', + ['a', 'b', '_:b0'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#first', '"x"@en-gb--ltr'], + ['_:b0', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#rest', 'http://www.w3.org/1999/02/22-rdf-syntax-ns#nil']), + ); + it( 'should parse statements with a multi-element list in the subject', shouldParse('( ) .', @@ -2428,6 +2458,14 @@ describe('Parser', () => { ), ); + it( + 'should parse literals with language and direction as subject', + shouldParse(parser, ' {"bonjour"@fr--ltr "hello"@en--rtl}.', + ['a', 'b', '_:b0'], + ['"bonjour"@fr--ltr', 'sameAs', '"hello"@en--rtl', '_:b0'], + ), + ); + it( 'should not parse RDF-star in the subject position', shouldNotParse(parser, '<< >> .', diff --git a/test/N3Store-test.js b/test/N3Store-test.js index 245dd045..dd6864e1 100644 --- a/test/N3Store-test.js +++ b/test/N3Store-test.js @@ -1722,7 +1722,7 @@ describe('Store', () => { b0: [ { termType: 'NamedNode', value: 'element1' }, { termType: 'Literal', value: 'element2', - language: '', datatype: { termType: 'NamedNode', value: namespaces.xsd.string } }, + language: '', direction: '', datatype: { termType: 'NamedNode', value: namespaces.xsd.string } }, ], }; @@ -1761,7 +1761,7 @@ describe('Store', () => { b0: [ { termType: 'NamedNode', value: 'element1' }, { termType: 'Literal', value: 'element2', - language: '', datatype: { termType: 'NamedNode', value: namespaces.xsd.string } }, + language: '', direction: '', datatype: { termType: 'NamedNode', value: namespaces.xsd.string } }, ], }; diff --git a/test/N3Writer-test.js b/test/N3Writer-test.js index 8870a3cf..dd6b5e95 100644 --- a/test/N3Writer-test.js +++ b/test/N3Writer-test.js @@ -76,6 +76,12 @@ describe('Writer', () => { ' "cde"@en-us.\n'), ); + it( + 'should serialize a literal with a language and direction', + shouldSerialize(['a', 'b', '"cde"@en-us--ltr'], + ' "cde"@en-us--ltr.\n'), + ); + // e.g. http://vocab.getty.edu/aat/300264727.ttl it( 'should serialize a literal with an artificial language', diff --git a/test/Term-test.js b/test/Term-test.js index 41743515..8cb8ebc5 100644 --- a/test/Term-test.js +++ b/test/Term-test.js @@ -128,6 +128,7 @@ describe('Term', () => { termType: 'Literal', value: 'abc', language: 'en-us', + direction: '', datatype: { termType: 'NamedNode', value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#langString', @@ -136,6 +137,22 @@ describe('Term', () => { }, ); + it( + /**/'should create a Literal from a string that starts with a quotation mark and has a direction', + () => { + expect(termFromId('"abc"@en-us--rtl').toJSON()).toEqual({ + termType: 'Literal', + value: 'abc', + language: 'en-us', + direction: 'rtl', + datatype: { + termType: 'NamedNode', + value: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#dirLangString', + }, + }); + }, + ); + it( 'should create a Quad with the default graph if the id doesnt specify the graph', () => { @@ -277,6 +294,10 @@ describe('Term', () => { expect(termFromId('"abc"@en-us', factory)).toEqual(['l', 'abc', 'en-us']); }); + it('should create a Literal with a language and direction', () => { + expect(termFromId('"abc"@en-us--rtl', factory)).toEqual(['l', 'abc', { language: 'en-us', direction: 'rtl' }]); + }); + it('should create a Literal with a datatype', () => { expect(termFromId('"abc"^^https://ex.org/type', factory)).toEqual(['l', 'abc', ['n', 'https://ex.org/type']]); }); @@ -348,6 +369,14 @@ describe('Term', () => { }, ); + it( + 'should create an id that starts with a quotation mark and language tag from a Literal with a language and direction', + () => { + expect(termToId(new Literal('"abc"@en-us--rtl'))).toBe('"abc"@en-us--rtl'); + expect(termToId(new Literal('"abc"@en-us--rtl').toJSON())).toBe('"abc"@en-us--rtl'); + }, + ); + it( 'should create an id that starts with a quotation mark and language tag from a Literal string with a language', () => { @@ -355,6 +384,13 @@ describe('Term', () => { }, ); + it( + 'should create an id that starts with a quotation mark and language tag from a Literal string with a language and direction', + () => { + expect(termToId('"abc"@en-us--rtl')).toBe('"abc"@en-us--rtl'); + }, + ); + it( 'should create an id that starts with a quotation mark, datatype and language tag from a Literal with a datatype and language', () => {