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/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.filename);
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, filename = '<input>') {
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 \${esc} escape`, startLine, startColumn);
1334
+ if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
1258
1335
  hex += advance();
1259
1336
  }
1260
- return String.fromCodePoint(Number.parseInt(hex, 16));
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 += advance();
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 (/\s/.test(c) || '{}()[].,;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
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
- const map = { n: '\n', r: '\r', t: '\t', b: '\b', f: '\f', '"': '"', "'": "'", '\\': '\\' };
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.6",
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 test/shacl12-rules.test.js",
42
- "w3c:rdf": "node test/w3c-rdf.test.js",
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"