eyeleng 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +275 -18
- package/dist/browser/eyeleng.browser.js +123 -13
- package/eyeleng.js +123 -13
- package/package.json +7 -4
- package/playground.html +16 -4
- package/reports/w3c-rdf-earl.ttl +11707 -0
- package/src/parser.js +76 -2
- package/src/rdfManifest.js +13 -0
- package/src/shacl12RulesManifest.js +386 -0
- package/src/tokenizer.js +47 -11
- package/test/api.test.js +32 -0
- package/test/browser-bundle.test.js +8 -0
- package/test/shacl12-rules.test.js +29 -215
- package/test/w3c-rdf.test.js +2 -2
- package/tools/bundle.js +0 -17
- package/tools/w3c-rdf.js +20 -1
- package/tools/w3c-shacl12-rules.js +55 -0
- package/HANDBOOK.md +0 -1070
package/eyeleng.js
CHANGED
|
@@ -396,7 +396,7 @@
|
|
|
396
396
|
|
|
397
397
|
class Parser {
|
|
398
398
|
constructor(source, options = {}) {
|
|
399
|
-
this.tokens = Array.isArray(source) ? source : tokenize(source, options
|
|
399
|
+
this.tokens = Array.isArray(source) ? source : tokenize(source, options);
|
|
400
400
|
this.pos = 0;
|
|
401
401
|
this.options = options;
|
|
402
402
|
this.baseIRI = options.baseIRI || null;
|
|
@@ -459,6 +459,7 @@
|
|
|
459
459
|
let name = nameToken.value;
|
|
460
460
|
if (!name.endsWith(':')) throw this.error('Prefix name must end with :', nameToken);
|
|
461
461
|
name = name.slice(0, -1);
|
|
462
|
+
if (this.strictGrammar() && !isValidPNPrefix(name)) throw this.error(`Invalid prefix name ${nameToken.value}`, nameToken);
|
|
462
463
|
const iriToken = this.expectType('iri');
|
|
463
464
|
this.prefixes[name] = this.resolveIRI(iriToken.value, iriToken);
|
|
464
465
|
if (wasAtPrefix) this.consumeOptionalDot();
|
|
@@ -466,6 +467,10 @@
|
|
|
466
467
|
|
|
467
468
|
parseVersion() {
|
|
468
469
|
const token = this.expectType('string');
|
|
470
|
+
if (this.strictGrammar()) {
|
|
471
|
+
if (token.long) throw this.error('VERSION must use a short string literal', token);
|
|
472
|
+
if (token.value !== '1.2') throw this.error('VERSION must be the SHACL Rules version label \"1.2\"', token);
|
|
473
|
+
}
|
|
469
474
|
this.version = token.value;
|
|
470
475
|
}
|
|
471
476
|
|
|
@@ -861,6 +866,7 @@
|
|
|
861
866
|
} else if (this.matchWord('SET')) {
|
|
862
867
|
clauses.push(this.parseSetClause());
|
|
863
868
|
} else if (this.matchWord('BIND')) {
|
|
869
|
+
if (this.strictGrammar()) throw this.error('BIND is not part of the SHACL 1.2 Rules grammar; use SET');
|
|
864
870
|
clauses.push(this.parseBindClause());
|
|
865
871
|
} else if (this.matchWord('NOT')) {
|
|
866
872
|
this.expectValue('{');
|
|
@@ -968,8 +974,9 @@
|
|
|
968
974
|
if (colon < 0) throw this.error(`Expected IRI, prefixed name, literal, blank node, or variable; got ${value}`, token);
|
|
969
975
|
const prefix = value.slice(0, colon);
|
|
970
976
|
const local = value.slice(colon + 1);
|
|
977
|
+
if (this.strictGrammar()) validatePrefixedName(prefix, local, value, token, (message, errToken) => this.error(message, errToken));
|
|
971
978
|
if (!(prefix in this.prefixes)) throw this.error(`Unknown prefix ${prefix}:`, token);
|
|
972
|
-
return this.prefixes[prefix] + local;
|
|
979
|
+
return this.prefixes[prefix] + decodePNLocalEscapes(local);
|
|
973
980
|
}
|
|
974
981
|
|
|
975
982
|
resolveIRI(value, token = null) {
|
|
@@ -1128,9 +1135,76 @@
|
|
|
1128
1135
|
peek() { return this.tokens[this.pos]; }
|
|
1129
1136
|
peekN(n) { return this.tokens[this.pos + n] || this.tokens[this.tokens.length - 1]; }
|
|
1130
1137
|
previous() { return this.tokens[this.pos - 1]; }
|
|
1138
|
+
strictGrammar() { return !!this.options.strictGrammar; }
|
|
1131
1139
|
error(message, token = this.peek()) { return new SyntaxErrorWithLocation(message, token); }
|
|
1132
1140
|
}
|
|
1133
1141
|
|
|
1142
|
+
|
|
1143
|
+
function isPnCharsBase(ch) {
|
|
1144
|
+
if (!ch) return false;
|
|
1145
|
+
return /[A-Za-z]/.test(ch) || ch.codePointAt(0) >= 0x00C0;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function isPnCharsU(ch) {
|
|
1149
|
+
return isPnCharsBase(ch) || ch === '_';
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
function isPnChars(ch) {
|
|
1153
|
+
return isPnCharsU(ch) || /[0-9-]/.test(ch) || ch === '\u00B7' || /[\u0300-\u036F\u203F-\u2040]/u.test(ch);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
function isValidPNPrefix(prefix) {
|
|
1157
|
+
if (prefix === '') return true;
|
|
1158
|
+
const chars = Array.from(prefix);
|
|
1159
|
+
if (!isPnCharsBase(chars[0])) return false;
|
|
1160
|
+
if (chars.length > 1 && chars.at(-1) === '.') return false;
|
|
1161
|
+
return chars.slice(1).every((ch) => isPnChars(ch) || ch === '.');
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
function plxLength(text, index) {
|
|
1165
|
+
const ch = text[index];
|
|
1166
|
+
if (ch === '%' && /[0-9A-Fa-f]/.test(text[index + 1] || '') && /[0-9A-Fa-f]/.test(text[index + 2] || '')) return 3;
|
|
1167
|
+
if (ch === '\\' && /[_~.!$&'()*+,;=/?#@%-]/.test(text[index + 1] || '')) return 2;
|
|
1168
|
+
return 0;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
function isPNLocalStartAt(text, index) {
|
|
1172
|
+
const ch = text[index];
|
|
1173
|
+
return isPnCharsU(ch) || /[0-9:]/.test(ch || '') || plxLength(text, index) > 0;
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function isPNLocalBodyAt(text, index) {
|
|
1177
|
+
const ch = text[index];
|
|
1178
|
+
return isPnChars(ch) || ch === '.' || ch === ':' || plxLength(text, index) > 0;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function isPNLocalEndAt(text, index) {
|
|
1182
|
+
const ch = text[index];
|
|
1183
|
+
return isPnChars(ch) || ch === ':' || plxLength(text, index) > 0;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function validatePNLocal(local) {
|
|
1187
|
+
if (local === '') return true;
|
|
1188
|
+
if (!isPNLocalStartAt(local, 0)) return false;
|
|
1189
|
+
let lastStart = 0;
|
|
1190
|
+
for (let i = 0; i < local.length;) {
|
|
1191
|
+
const len = plxLength(local, i) || 1;
|
|
1192
|
+
if (i > 0 && !isPNLocalBodyAt(local, i)) return false;
|
|
1193
|
+
lastStart = i;
|
|
1194
|
+
i += len;
|
|
1195
|
+
}
|
|
1196
|
+
return isPNLocalEndAt(local, lastStart);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function validatePrefixedName(prefix, local, value, token, makeError) {
|
|
1200
|
+
if (!isValidPNPrefix(prefix)) throw makeError(`Invalid prefixed name ${value}: invalid prefix`, token);
|
|
1201
|
+
if (!validatePNLocal(local)) throw makeError(`Invalid prefixed name ${value}: invalid local name`, token);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function decodePNLocalEscapes(local) {
|
|
1205
|
+
return String(local).replace(/\\([_~.!$&'()*+,;=/?#@%-])/g, '$1');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1134
1208
|
function numericLiteral(value) {
|
|
1135
1209
|
if (Number.isInteger(value)) return literal(value, XSD_INTEGER);
|
|
1136
1210
|
return literal(value, XSD_DECIMAL);
|
|
@@ -1200,7 +1274,10 @@
|
|
|
1200
1274
|
}
|
|
1201
1275
|
}
|
|
1202
1276
|
|
|
1203
|
-
function tokenize(source,
|
|
1277
|
+
function tokenize(source, filenameOrOptions = '<input>') {
|
|
1278
|
+
const options = typeof filenameOrOptions === 'object' && filenameOrOptions !== null ? filenameOrOptions : { filename: filenameOrOptions };
|
|
1279
|
+
const filename = options.filename || '<input>';
|
|
1280
|
+
const strictGrammar = !!options.strictGrammar;
|
|
1204
1281
|
const tokens = [];
|
|
1205
1282
|
let i = 0;
|
|
1206
1283
|
let line = 1;
|
|
@@ -1215,8 +1292,8 @@
|
|
|
1215
1292
|
else column += 1;
|
|
1216
1293
|
return ch;
|
|
1217
1294
|
}
|
|
1218
|
-
function token(type, value, startLine, startColumn) {
|
|
1219
|
-
tokens.push({ type, value, line: startLine, column: startColumn, filename });
|
|
1295
|
+
function token(type, value, startLine, startColumn, extra = {}) {
|
|
1296
|
+
tokens.push({ type, value, line: startLine, column: startColumn, filename, ...extra });
|
|
1220
1297
|
}
|
|
1221
1298
|
function syntax(message, startLine, startColumn) {
|
|
1222
1299
|
throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });
|
|
@@ -1254,14 +1331,37 @@
|
|
|
1254
1331
|
const length = esc === 'u' ? 4 : 8;
|
|
1255
1332
|
let hex = '';
|
|
1256
1333
|
for (let j = 0; j < length; j += 1) {
|
|
1257
|
-
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid
|
|
1334
|
+
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
1258
1335
|
hex += advance();
|
|
1259
1336
|
}
|
|
1260
|
-
|
|
1337
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
1338
|
+
try { return String.fromCodePoint(codePoint); }
|
|
1339
|
+
catch { syntax(`Invalid \\${esc} escape`, startLine, startColumn); }
|
|
1261
1340
|
}
|
|
1341
|
+
if (strictGrammar && !Object.hasOwn(escapeMap, esc)) syntax(`Invalid escape \\${esc}`, startLine, startColumn);
|
|
1262
1342
|
return escapeValue(esc);
|
|
1263
1343
|
}
|
|
1264
1344
|
|
|
1345
|
+
function readIriChar(startLine, startColumn) {
|
|
1346
|
+
if (current() === '\\') {
|
|
1347
|
+
advance();
|
|
1348
|
+
const esc = advance();
|
|
1349
|
+
if (esc !== 'u' && esc !== 'U') syntax(`Invalid IRI escape \\${esc}`, startLine, startColumn);
|
|
1350
|
+
const length = esc === 'u' ? 4 : 8;
|
|
1351
|
+
let hex = '';
|
|
1352
|
+
for (let j = 0; j < length; j += 1) {
|
|
1353
|
+
if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
|
|
1354
|
+
hex += advance();
|
|
1355
|
+
}
|
|
1356
|
+
const codePoint = Number.parseInt(hex, 16);
|
|
1357
|
+
try { return String.fromCodePoint(codePoint); }
|
|
1358
|
+
catch { syntax(`Invalid \\${esc} escape`, startLine, startColumn); }
|
|
1359
|
+
}
|
|
1360
|
+
const c = current();
|
|
1361
|
+
if (strictGrammar && (/[\u0000-\u0020]/.test(c) || /[<>"{}|^`]/.test(c))) syntax(`Invalid character in IRI reference ${JSON.stringify(c)}`, startLine, startColumn);
|
|
1362
|
+
return advance();
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1265
1365
|
while (i < source.length) {
|
|
1266
1366
|
const ch = current();
|
|
1267
1367
|
if (/\s/.test(ch)) { advance(); continue; }
|
|
@@ -1307,7 +1407,7 @@
|
|
|
1307
1407
|
if (ch === '<' && looksLikeIRI(source, i)) {
|
|
1308
1408
|
let value = '';
|
|
1309
1409
|
advance();
|
|
1310
|
-
while (i < source.length && current() !== '>') value +=
|
|
1410
|
+
while (i < source.length && current() !== '>') value += readIriChar(startLine, startColumn);
|
|
1311
1411
|
if (current() !== '>') syntax('Unterminated IRI', startLine, startColumn);
|
|
1312
1412
|
advance();
|
|
1313
1413
|
token('iri', value, startLine, startColumn);
|
|
@@ -1327,7 +1427,7 @@
|
|
|
1327
1427
|
}
|
|
1328
1428
|
if (!startsWith(quote.repeat(3))) syntax('Unterminated long string literal', startLine, startColumn);
|
|
1329
1429
|
advance(); advance(); advance();
|
|
1330
|
-
token('string', value, startLine, startColumn);
|
|
1430
|
+
token('string', value, startLine, startColumn, { long: true, quote });
|
|
1331
1431
|
continue;
|
|
1332
1432
|
}
|
|
1333
1433
|
|
|
@@ -1345,7 +1445,7 @@
|
|
|
1345
1445
|
}
|
|
1346
1446
|
if (current() !== quote) syntax('Unterminated string literal', startLine, startColumn);
|
|
1347
1447
|
advance();
|
|
1348
|
-
token('string', value, startLine, startColumn);
|
|
1448
|
+
token('string', value, startLine, startColumn, { long: false, quote });
|
|
1349
1449
|
continue;
|
|
1350
1450
|
}
|
|
1351
1451
|
|
|
@@ -1391,7 +1491,16 @@
|
|
|
1391
1491
|
let value = '';
|
|
1392
1492
|
while (i < source.length) {
|
|
1393
1493
|
const c = current();
|
|
1394
|
-
if (
|
|
1494
|
+
if (c === '\\' && peek() !== undefined) {
|
|
1495
|
+
value += advance();
|
|
1496
|
+
value += advance();
|
|
1497
|
+
continue;
|
|
1498
|
+
}
|
|
1499
|
+
if (/\s/.test(c) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
|
|
1500
|
+
if (c === '.') {
|
|
1501
|
+
const n = peek();
|
|
1502
|
+
if (n === undefined || /\s/.test(n) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;
|
|
1503
|
+
}
|
|
1395
1504
|
if (c === '#') break;
|
|
1396
1505
|
value += advance();
|
|
1397
1506
|
}
|
|
@@ -1424,9 +1533,10 @@
|
|
|
1424
1533
|
return false;
|
|
1425
1534
|
}
|
|
1426
1535
|
|
|
1536
|
+
const escapeMap = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', '"': '"', "'": "'", '\\': '\\' };
|
|
1537
|
+
|
|
1427
1538
|
function escapeValue(esc) {
|
|
1428
|
-
|
|
1429
|
-
return map[esc] ?? esc;
|
|
1539
|
+
return escapeMap[esc] ?? esc;
|
|
1430
1540
|
}
|
|
1431
1541
|
|
|
1432
1542
|
module.exports = { tokenize, SyntaxErrorWithLocation };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eyeleng",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "The EYE Logic Engine: a JavaScript implementation of SHACL Rules, including SRL and RDF Rules syntax front-ends.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"SRL",
|
|
@@ -37,12 +37,15 @@
|
|
|
37
37
|
"pretest": "node tools/bundle.js",
|
|
38
38
|
"test": "node test/run.js",
|
|
39
39
|
"preversion": "npm test",
|
|
40
|
+
"version": "node tools/bundle.js",
|
|
40
41
|
"postversion": "git push origin HEAD --follow-tags",
|
|
41
|
-
"w3c:rules": "node
|
|
42
|
-
"w3c:rdf": "node
|
|
42
|
+
"w3c:rules": "node tools/w3c-shacl12-rules.js",
|
|
43
|
+
"w3c:rdf": "node tools/w3c-rdf.js",
|
|
43
44
|
"w3c:rdf:json": "node tools/w3c-rdf.js --json",
|
|
44
45
|
"w3c:rdf:earl": "node tools/w3c-rdf.js --earl",
|
|
45
|
-
"w3c:all": "npm run w3c:rules && npm run w3c:rdf"
|
|
46
|
+
"w3c:all": "npm run w3c:rules && npm run w3c:rdf",
|
|
47
|
+
"w3c:rules:json": "node tools/w3c-shacl12-rules.js --json",
|
|
48
|
+
"w3c:rules:earl": "node tools/w3c-shacl12-rules.js --earl"
|
|
46
49
|
},
|
|
47
50
|
"publishConfig": {
|
|
48
51
|
"access": "public"
|