eyeling 1.7.19 → 1.8.0
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 +2 -1
- package/examples/it-is-about-time.n3 +29 -0
- package/examples/it-is-about-time.srl +36 -0
- package/examples/output/it-is-about-time.n3 +7 -0
- package/eyeling-builtins.ttl +24 -0
- package/eyeling.js +476 -13
- package/index.js +5 -1
- package/package.json +1 -1
- package/test/api.test.js +103 -2
package/README.md
CHANGED
|
@@ -21,7 +21,7 @@ Try it here:
|
|
|
21
21
|
- Load an N3 program from a URL (in the "Load N3 from URL" box or as ?url=...).
|
|
22
22
|
- Share a link with the program encoded in the URL fragment (`#...`).
|
|
23
23
|
|
|
24
|
-
-
|
|
24
|
+
- Streaming demo
|
|
25
25
|
- Browse a Wikidata entity, load its facts, and see Eyeling’s **deductive closure appear incrementally** as triples are derived.
|
|
26
26
|
- Edit **N3 rules live** and re-run to watch how different inference rules change what gets derived.
|
|
27
27
|
- Demo **CORS-safe dynamic fetching**: derived “fetch requests” can trigger extra facts (e.g., Wikiquote extracts) that are injected and re-reasoned.
|
|
@@ -122,6 +122,7 @@ Options:
|
|
|
122
122
|
-a, --ast Print parsed AST as JSON and exit.
|
|
123
123
|
--strings Print log:outputString strings (ordered by key) instead of N3 output.
|
|
124
124
|
--enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.
|
|
125
|
+
--stream Stream derived triples as soon as they are derived.
|
|
125
126
|
```
|
|
126
127
|
|
|
127
128
|
By default, `eyeling`:
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
@prefix ex: <http://example.org/> .
|
|
2
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
3
|
+
@prefix tfn: <https://w3id.org/time-fn#> .
|
|
4
|
+
@prefix math: <http://www.w3.org/2000/10/swap/math#> .
|
|
5
|
+
|
|
6
|
+
ex:e1 a ex:Event ;
|
|
7
|
+
ex:when "2000-01-02T03:04:05"^^xsd:dateTime ; # floating (no TZ)
|
|
8
|
+
ex:day "2000-01-02"^^xsd:date ;
|
|
9
|
+
ex:tz "Z" .
|
|
10
|
+
ex:e2 a ex:Event ;
|
|
11
|
+
ex:when "2000-01-02T03:04:05-03:00"^^xsd:dateTime ;
|
|
12
|
+
ex:day "2000-01-02"^^xsd:date ;
|
|
13
|
+
ex:tz "-03:00" .
|
|
14
|
+
|
|
15
|
+
{
|
|
16
|
+
?this ex:when ?whenRaw .
|
|
17
|
+
?this ex:day ?day .
|
|
18
|
+
?this ex:tz ?tz .
|
|
19
|
+
?startInc math:notGreaterThan ?whenBound .
|
|
20
|
+
?whenBound math:notGreaterThan ?endInc .
|
|
21
|
+
(?whenRaw ?tz) tfn:bindDefaultTimezone ?whenBound .
|
|
22
|
+
(?day) tfn:periodMinInclusive ?startInc .
|
|
23
|
+
(?day) tfn:periodMaxInclusive ?endInc .
|
|
24
|
+
(?day) tfn:periodMinExclusive ?startEx .
|
|
25
|
+
(?day) tfn:periodMaxExclusive ?endEx .
|
|
26
|
+
} => {
|
|
27
|
+
?this ex:occursOn ?day .
|
|
28
|
+
?this ex:whenBound ?whenBound .
|
|
29
|
+
} .
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
PREFIX ex: <http://example.org/>
|
|
2
|
+
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
|
|
3
|
+
PREFIX tfn: <https://w3id.org/time-fn#>
|
|
4
|
+
|
|
5
|
+
DATA {
|
|
6
|
+
ex:e1 a ex:Event ;
|
|
7
|
+
ex:when "2000-01-02T03:04:05"^^xsd:dateTime ; # floating (no TZ)
|
|
8
|
+
ex:day "2000-01-02"^^xsd:date ;
|
|
9
|
+
ex:tz "Z" .
|
|
10
|
+
|
|
11
|
+
ex:e2 a ex:Event ;
|
|
12
|
+
ex:when "2000-01-02T03:04:05-03:00"^^xsd:dateTime ;
|
|
13
|
+
ex:day "2000-01-02"^^xsd:date ;
|
|
14
|
+
ex:tz "-03:00" .
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
RULE {
|
|
18
|
+
$this ex:occursOn ?day .
|
|
19
|
+
$this ex:whenBound ?whenBound .
|
|
20
|
+
}
|
|
21
|
+
WHERE {
|
|
22
|
+
$this ex:when ?whenRaw ;
|
|
23
|
+
ex:day ?day ;
|
|
24
|
+
ex:tz ?tz .
|
|
25
|
+
|
|
26
|
+
BIND( tfn:bindDefaultTimezone(?whenRaw, ?tz) AS ?whenBound )
|
|
27
|
+
|
|
28
|
+
BIND( tfn:periodMinInclusive(?day) AS ?startInc )
|
|
29
|
+
BIND( tfn:periodMaxInclusive(?day) AS ?endInc )
|
|
30
|
+
BIND( tfn:periodMinExclusive(?day) AS ?startEx )
|
|
31
|
+
BIND( tfn:periodMaxExclusive(?day) AS ?endEx )
|
|
32
|
+
|
|
33
|
+
# Inclusive containment: start <= whenBound <= end
|
|
34
|
+
FILTER( ?startInc <= ?whenBound && ?whenBound <= ?endInc )
|
|
35
|
+
}
|
|
36
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
@prefix ex: <http://example.org/> .
|
|
2
|
+
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
3
|
+
|
|
4
|
+
ex:e1 ex:occursOn "2000-01-02"^^xsd:date .
|
|
5
|
+
ex:e1 ex:whenBound "2000-01-02T03:04:05Z"^^xsd:dateTime .
|
|
6
|
+
ex:e2 ex:occursOn "2000-01-02"^^xsd:date .
|
|
7
|
+
ex:e2 ex:whenBound "2000-01-02T03:04:05-03:00"^^xsd:dateTime .
|
package/eyeling-builtins.ttl
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
|
|
9
9
|
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
|
|
10
10
|
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
|
|
11
|
+
@prefix tfn: <https://w3id.org/time-fn#> .
|
|
11
12
|
|
|
12
13
|
# ------------------------------------------------------------------------------
|
|
13
14
|
# Builtin catalog (eyeling.js)
|
|
@@ -336,3 +337,26 @@ string:format a ex:Builtin ; ex:kind ex:Function ;
|
|
|
336
337
|
|
|
337
338
|
string:jsonPointer a ex:Builtin ; ex:kind ex:Function ;
|
|
338
339
|
rdfs:comment "JSON Pointer lookup: (jsonText pointer) -> value term. Expects rdf:JSON-typed literal (or treated as rdf:JSON); caches parsed JSON and pointer lookups." .
|
|
340
|
+
|
|
341
|
+
# --- tfn: (Time Functions) -------------------------------------------
|
|
342
|
+
# Source semantics: Smessaert et al., “It’s About Time: Time Functions for Comparing Partial and Floating Time Literals in SPARQL”, §2.2.
|
|
343
|
+
|
|
344
|
+
tfn:periodMinInclusive a ex:Builtin ;
|
|
345
|
+
ex:kind ex:Function ;
|
|
346
|
+
rdfs:comment "Time Functions: inclusive lower bound of the period represented by a temporal literal (xsd:date, xsd:gYearMonth, xsd:gYear, xsd:dateTime, ...), returned as xsd:dateTime." .
|
|
347
|
+
|
|
348
|
+
tfn:periodMaxInclusive a ex:Builtin ;
|
|
349
|
+
ex:kind ex:Function ;
|
|
350
|
+
rdfs:comment "Time Functions: inclusive upper bound of the period represented by a temporal literal, returned as xsd:dateTime." .
|
|
351
|
+
|
|
352
|
+
tfn:periodMinExclusive a ex:Builtin ;
|
|
353
|
+
ex:kind ex:Function ;
|
|
354
|
+
rdfs:comment "Time Functions: exclusive lower bound of the period represented by a temporal literal, returned as xsd:dateTime." .
|
|
355
|
+
|
|
356
|
+
tfn:periodMaxExclusive a ex:Builtin ;
|
|
357
|
+
ex:kind ex:Function ;
|
|
358
|
+
rdfs:comment "Time Functions: exclusive upper bound of the period represented by a temporal literal, returned as xsd:dateTime." .
|
|
359
|
+
|
|
360
|
+
tfn:bindDefaultTimezone a ex:Builtin ;
|
|
361
|
+
ex:kind ex:Function ;
|
|
362
|
+
rdfs:comment "Time Functions: binds a default timezone to a floating time literal. Input is (timeLiteral timeZone). If the literal already has a timezone, it is returned unchanged; otherwise the specified timezone is applied." .
|
package/eyeling.js
CHANGED
|
@@ -42,6 +42,7 @@ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
|
|
|
42
42
|
const CRYPTO_NS = 'http://www.w3.org/2000/10/swap/crypto#';
|
|
43
43
|
const MATH_NS = 'http://www.w3.org/2000/10/swap/math#';
|
|
44
44
|
const TIME_NS = 'http://www.w3.org/2000/10/swap/time#';
|
|
45
|
+
const TFN_NS = 'https://w3id.org/time-fn#';
|
|
45
46
|
const LIST_NS = 'http://www.w3.org/2000/10/swap/list#';
|
|
46
47
|
const LOG_NS = 'http://www.w3.org/2000/10/swap/log#';
|
|
47
48
|
const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
|
|
@@ -110,9 +111,7 @@ let enforceHttpsEnabled = false;
|
|
|
110
111
|
|
|
111
112
|
function __maybeEnforceHttps(iri) {
|
|
112
113
|
if (!enforceHttpsEnabled) return iri;
|
|
113
|
-
return typeof iri === 'string' && iri.startsWith('http://')
|
|
114
|
-
? 'https://' + iri.slice('http://'.length)
|
|
115
|
-
: iri;
|
|
114
|
+
return typeof iri === 'string' && iri.startsWith('http://') ? 'https://' + iri.slice('http://'.length) : iri;
|
|
116
115
|
}
|
|
117
116
|
|
|
118
117
|
// Environment detection (Node vs Browser/Worker).
|
|
@@ -426,12 +425,7 @@ let __tracePrefixes = null;
|
|
|
426
425
|
function __traceWriteLine(line) {
|
|
427
426
|
// Prefer stderr in Node, fall back to console.error elsewhere.
|
|
428
427
|
try {
|
|
429
|
-
if (
|
|
430
|
-
__IS_NODE &&
|
|
431
|
-
typeof process !== 'undefined' &&
|
|
432
|
-
process.stderr &&
|
|
433
|
-
typeof process.stderr.write === 'function'
|
|
434
|
-
) {
|
|
428
|
+
if (__IS_NODE && typeof process !== 'undefined' && process.stderr && typeof process.stderr.write === 'function') {
|
|
435
429
|
process.stderr.write(String(line) + '\n');
|
|
436
430
|
return;
|
|
437
431
|
}
|
|
@@ -3108,7 +3102,6 @@ function termToJsString(t) {
|
|
|
3108
3102
|
return typeof lex === 'string' ? lex : String(lex);
|
|
3109
3103
|
}
|
|
3110
3104
|
|
|
3111
|
-
|
|
3112
3105
|
function makeStringLiteral(str) {
|
|
3113
3106
|
// JSON.stringify gives us a valid N3/Turtle-style quoted string
|
|
3114
3107
|
// (with proper escaping for quotes, backslashes, newlines, …).
|
|
@@ -3625,6 +3618,285 @@ function parseXsdDateTimeLexParts(t) {
|
|
|
3625
3618
|
return { yearStr, month, day, hour, minute, second, tz };
|
|
3626
3619
|
}
|
|
3627
3620
|
|
|
3621
|
+
// -----------------------------------------------------------------------------
|
|
3622
|
+
// Time Functions (tfn:) helpers
|
|
3623
|
+
// -----------------------------------------------------------------------------
|
|
3624
|
+
|
|
3625
|
+
function __tfnParseTimezoneValue(t) {
|
|
3626
|
+
// Accept plain string literals and xsd:string. Return "Z" or "+hh:mm" or "-hh:mm".
|
|
3627
|
+
if (!(t instanceof Literal)) return null;
|
|
3628
|
+
const [lex, dt] = literalParts(t.value);
|
|
3629
|
+
const s = stripQuotes(lex).trim();
|
|
3630
|
+
if (!s) return null;
|
|
3631
|
+
if (s === 'Z') return 'Z';
|
|
3632
|
+
const m = /^([+-])(\d{2}):(\d{2})$/.exec(s);
|
|
3633
|
+
if (!m) return null;
|
|
3634
|
+
const hh = parseInt(m[2], 10);
|
|
3635
|
+
const mm = parseInt(m[3], 10);
|
|
3636
|
+
if (!(hh >= 0 && hh <= 14)) return null;
|
|
3637
|
+
if (!(mm >= 0 && mm <= 59)) return null;
|
|
3638
|
+
// disallow offsets beyond 14:00 (e.g., +14:30)
|
|
3639
|
+
if (hh === 14 && mm !== 0) return null;
|
|
3640
|
+
return `${m[1]}${m[2]}:${m[3]}`;
|
|
3641
|
+
}
|
|
3642
|
+
|
|
3643
|
+
function __tfnHasTimezoneSuffix(s) {
|
|
3644
|
+
return /(Z|[+-]\d{2}:\d{2})$/.test(s);
|
|
3645
|
+
}
|
|
3646
|
+
|
|
3647
|
+
function __tfnParseTemporalLiteralParts(t) {
|
|
3648
|
+
if (!(t instanceof Literal)) return null;
|
|
3649
|
+
const [lex, dt] = literalParts(t.value);
|
|
3650
|
+
if (!dt) return null;
|
|
3651
|
+
const val = stripQuotes(lex);
|
|
3652
|
+
|
|
3653
|
+
// xsd:dateTime
|
|
3654
|
+
if (dt === XSD_NS + 'dateTime') {
|
|
3655
|
+
const m = /^(-?\d{4,})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d+))?(Z|[+-]\d{2}:\d{2})?$/.exec(val);
|
|
3656
|
+
if (!m) return null;
|
|
3657
|
+
const year = BigInt(m[1]);
|
|
3658
|
+
const month = parseInt(m[2], 10);
|
|
3659
|
+
const day = parseInt(m[3], 10);
|
|
3660
|
+
const hour = parseInt(m[4], 10);
|
|
3661
|
+
const minute = parseInt(m[5], 10);
|
|
3662
|
+
const second = parseInt(m[6], 10);
|
|
3663
|
+
const frac = m[7] || '';
|
|
3664
|
+
const tz = m[8] || null;
|
|
3665
|
+
|
|
3666
|
+
if (!(month >= 1 && month <= 12)) return null;
|
|
3667
|
+
if (!(day >= 1 && day <= 31)) return null;
|
|
3668
|
+
if (!(hour >= 0 && hour <= 23)) return null;
|
|
3669
|
+
if (!(minute >= 0 && minute <= 59)) return null;
|
|
3670
|
+
if (!(second >= 0 && second <= 59)) return null;
|
|
3671
|
+
|
|
3672
|
+
// Keep milliseconds precision (3 digits), truncating extra digits.
|
|
3673
|
+
const msStr = (frac + '000').slice(0, 3);
|
|
3674
|
+
const millis = parseInt(msStr, 10);
|
|
3675
|
+
|
|
3676
|
+
return { kind: 'dateTime', year, month, day, hour, minute, second, millis, tz, dt };
|
|
3677
|
+
}
|
|
3678
|
+
|
|
3679
|
+
// xsd:date
|
|
3680
|
+
if (dt === XSD_NS + 'date') {
|
|
3681
|
+
const m = /^(-?\d{4,})-(\d{2})-(\d{2})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
|
|
3682
|
+
if (!m) return null;
|
|
3683
|
+
const year = BigInt(m[1]);
|
|
3684
|
+
const month = parseInt(m[2], 10);
|
|
3685
|
+
const day = parseInt(m[3], 10);
|
|
3686
|
+
const tz = m[4] || null;
|
|
3687
|
+
if (!(month >= 1 && month <= 12)) return null;
|
|
3688
|
+
if (!(day >= 1 && day <= 31)) return null;
|
|
3689
|
+
return { kind: 'date', year, month, day, tz, dt };
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// xsd:gYearMonth
|
|
3693
|
+
if (dt === XSD_NS + 'gYearMonth') {
|
|
3694
|
+
const m = /^(-?\d{4,})-(\d{2})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
|
|
3695
|
+
if (!m) return null;
|
|
3696
|
+
const year = BigInt(m[1]);
|
|
3697
|
+
const month = parseInt(m[2], 10);
|
|
3698
|
+
const tz = m[3] || null;
|
|
3699
|
+
if (!(month >= 1 && month <= 12)) return null;
|
|
3700
|
+
return { kind: 'gYearMonth', year, month, tz, dt };
|
|
3701
|
+
}
|
|
3702
|
+
|
|
3703
|
+
// xsd:gYear
|
|
3704
|
+
if (dt === XSD_NS + 'gYear') {
|
|
3705
|
+
const m = /^(-?\d{4,})(Z|[+-]\d{2}:\d{2})?$/.exec(val);
|
|
3706
|
+
if (!m) return null;
|
|
3707
|
+
const year = BigInt(m[1]);
|
|
3708
|
+
const tz = m[2] || null;
|
|
3709
|
+
return { kind: 'gYear', year, tz, dt };
|
|
3710
|
+
}
|
|
3711
|
+
|
|
3712
|
+
return null;
|
|
3713
|
+
}
|
|
3714
|
+
|
|
3715
|
+
function __tfnMod(a, b) {
|
|
3716
|
+
let r = a % b;
|
|
3717
|
+
if (r < 0n) r += b;
|
|
3718
|
+
return r;
|
|
3719
|
+
}
|
|
3720
|
+
|
|
3721
|
+
function __tfnIsLeapYear(y) {
|
|
3722
|
+
// Proleptic Gregorian leap-year rule.
|
|
3723
|
+
// Works for negative years using normalized modulus.
|
|
3724
|
+
if (__tfnMod(y, 4n) !== 0n) return false;
|
|
3725
|
+
if (__tfnMod(y, 100n) !== 0n) return true;
|
|
3726
|
+
return __tfnMod(y, 400n) === 0n;
|
|
3727
|
+
}
|
|
3728
|
+
|
|
3729
|
+
function __tfnDaysInMonth(y, m) {
|
|
3730
|
+
if (m === 2) return __tfnIsLeapYear(y) ? 29 : 28;
|
|
3731
|
+
if (m === 4 || m === 6 || m === 9 || m === 11) return 30;
|
|
3732
|
+
return 31;
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
function __tfnPad2(n) {
|
|
3736
|
+
return n < 10 ? '0' + String(n) : String(n);
|
|
3737
|
+
}
|
|
3738
|
+
function __tfnPad3(n) {
|
|
3739
|
+
const s = String(n);
|
|
3740
|
+
return s.length === 1 ? '00' + s : s.length === 2 ? '0' + s : s;
|
|
3741
|
+
}
|
|
3742
|
+
function __tfnFormatYear(y) {
|
|
3743
|
+
const neg = y < 0n;
|
|
3744
|
+
const abs = neg ? -y : y;
|
|
3745
|
+
let digits = abs.toString();
|
|
3746
|
+
if (digits.length < 4) digits = '0'.repeat(4 - digits.length) + digits;
|
|
3747
|
+
return neg ? '-' + digits : digits;
|
|
3748
|
+
}
|
|
3749
|
+
|
|
3750
|
+
function __tfnAdd1ms(c) {
|
|
3751
|
+
// Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
|
|
3752
|
+
if (c.millis < 999) {
|
|
3753
|
+
c.millis++;
|
|
3754
|
+
return c;
|
|
3755
|
+
}
|
|
3756
|
+
c.millis = 0;
|
|
3757
|
+
if (c.second < 59) {
|
|
3758
|
+
c.second++;
|
|
3759
|
+
return c;
|
|
3760
|
+
}
|
|
3761
|
+
c.second = 0;
|
|
3762
|
+
if (c.minute < 59) {
|
|
3763
|
+
c.minute++;
|
|
3764
|
+
return c;
|
|
3765
|
+
}
|
|
3766
|
+
c.minute = 0;
|
|
3767
|
+
if (c.hour < 23) {
|
|
3768
|
+
c.hour++;
|
|
3769
|
+
return c;
|
|
3770
|
+
}
|
|
3771
|
+
c.hour = 0;
|
|
3772
|
+
|
|
3773
|
+
const dim = __tfnDaysInMonth(c.year, c.month);
|
|
3774
|
+
if (c.day < dim) {
|
|
3775
|
+
c.day++;
|
|
3776
|
+
return c;
|
|
3777
|
+
}
|
|
3778
|
+
c.day = 1;
|
|
3779
|
+
if (c.month < 12) {
|
|
3780
|
+
c.month++;
|
|
3781
|
+
return c;
|
|
3782
|
+
}
|
|
3783
|
+
c.month = 1;
|
|
3784
|
+
c.year = c.year + 1n;
|
|
3785
|
+
return c;
|
|
3786
|
+
}
|
|
3787
|
+
|
|
3788
|
+
function __tfnSub1ms(c) {
|
|
3789
|
+
// Mutates and returns c; c: {year,month,day,hour,minute,second,millis}
|
|
3790
|
+
if (c.millis > 0) {
|
|
3791
|
+
c.millis--;
|
|
3792
|
+
return c;
|
|
3793
|
+
}
|
|
3794
|
+
c.millis = 999;
|
|
3795
|
+
if (c.second > 0) {
|
|
3796
|
+
c.second--;
|
|
3797
|
+
return c;
|
|
3798
|
+
}
|
|
3799
|
+
c.second = 59;
|
|
3800
|
+
if (c.minute > 0) {
|
|
3801
|
+
c.minute--;
|
|
3802
|
+
return c;
|
|
3803
|
+
}
|
|
3804
|
+
c.minute = 59;
|
|
3805
|
+
if (c.hour > 0) {
|
|
3806
|
+
c.hour--;
|
|
3807
|
+
return c;
|
|
3808
|
+
}
|
|
3809
|
+
c.hour = 23;
|
|
3810
|
+
|
|
3811
|
+
if (c.day > 1) {
|
|
3812
|
+
c.day--;
|
|
3813
|
+
return c;
|
|
3814
|
+
}
|
|
3815
|
+
// move to previous month
|
|
3816
|
+
if (c.month > 1) {
|
|
3817
|
+
c.month--;
|
|
3818
|
+
} else {
|
|
3819
|
+
c.month = 12;
|
|
3820
|
+
c.year = c.year - 1n;
|
|
3821
|
+
}
|
|
3822
|
+
c.day = __tfnDaysInMonth(c.year, c.month);
|
|
3823
|
+
return c;
|
|
3824
|
+
}
|
|
3825
|
+
|
|
3826
|
+
function __tfnMakeDateTimeLiteral(c, tz) {
|
|
3827
|
+
const y = __tfnFormatYear(c.year);
|
|
3828
|
+
const lex = `${y}-${__tfnPad2(c.month)}-${__tfnPad2(c.day)}T${__tfnPad2(c.hour)}:${__tfnPad2(c.minute)}:${__tfnPad2(c.second)}.${__tfnPad3(c.millis)}${tz}`;
|
|
3829
|
+
return internLiteral(`"${lex}"^^<${XSD_NS}dateTime>`);
|
|
3830
|
+
}
|
|
3831
|
+
|
|
3832
|
+
function __tfnComputePeriodBounds(parts) {
|
|
3833
|
+
// Returns { tzMin, tzMax, startC, endC } where startC/endC are component objects.
|
|
3834
|
+
const tzMin = parts.tz || '+14:00';
|
|
3835
|
+
const tzMax = parts.tz || '-14:00';
|
|
3836
|
+
|
|
3837
|
+
if (parts.kind === 'dateTime') {
|
|
3838
|
+
const base = {
|
|
3839
|
+
year: parts.year,
|
|
3840
|
+
month: parts.month,
|
|
3841
|
+
day: parts.day,
|
|
3842
|
+
hour: parts.hour,
|
|
3843
|
+
minute: parts.minute,
|
|
3844
|
+
second: parts.second,
|
|
3845
|
+
millis: parts.millis,
|
|
3846
|
+
};
|
|
3847
|
+
return { tzMin, tzMax, startC: { ...base }, endC: { ...base } };
|
|
3848
|
+
}
|
|
3849
|
+
|
|
3850
|
+
if (parts.kind === 'date') {
|
|
3851
|
+
const startC = { year: parts.year, month: parts.month, day: parts.day, hour: 0, minute: 0, second: 0, millis: 0 };
|
|
3852
|
+
const endC = {
|
|
3853
|
+
year: parts.year,
|
|
3854
|
+
month: parts.month,
|
|
3855
|
+
day: parts.day,
|
|
3856
|
+
hour: 23,
|
|
3857
|
+
minute: 59,
|
|
3858
|
+
second: 59,
|
|
3859
|
+
millis: 999,
|
|
3860
|
+
};
|
|
3861
|
+
return { tzMin, tzMax, startC, endC };
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
if (parts.kind === 'gYearMonth') {
|
|
3865
|
+
const dim = __tfnDaysInMonth(parts.year, parts.month);
|
|
3866
|
+
const startC = { year: parts.year, month: parts.month, day: 1, hour: 0, minute: 0, second: 0, millis: 0 };
|
|
3867
|
+
const endC = { year: parts.year, month: parts.month, day: dim, hour: 23, minute: 59, second: 59, millis: 999 };
|
|
3868
|
+
return { tzMin, tzMax, startC, endC };
|
|
3869
|
+
}
|
|
3870
|
+
|
|
3871
|
+
if (parts.kind === 'gYear') {
|
|
3872
|
+
const startC = { year: parts.year, month: 1, day: 1, hour: 0, minute: 0, second: 0, millis: 0 };
|
|
3873
|
+
const endC = { year: parts.year, month: 12, day: 31, hour: 23, minute: 59, second: 59, millis: 999 };
|
|
3874
|
+
return { tzMin, tzMax, startC, endC };
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
function __tfnBindDefaultTimezone(timeLit, tzLit) {
|
|
3881
|
+
if (!(timeLit instanceof Literal) || !(tzLit instanceof Literal)) return null;
|
|
3882
|
+
const tz = __tfnParseTimezoneValue(tzLit);
|
|
3883
|
+
if (!tz) return null;
|
|
3884
|
+
|
|
3885
|
+
const [lex, dt] = literalParts(timeLit.value);
|
|
3886
|
+
if (!dt) return null;
|
|
3887
|
+
const v = stripQuotes(lex);
|
|
3888
|
+
|
|
3889
|
+
// If already has tz, return unchanged.
|
|
3890
|
+
if (__tfnHasTimezoneSuffix(v)) return timeLit;
|
|
3891
|
+
|
|
3892
|
+
// Only support the temporal types we parse.
|
|
3893
|
+
if (dt !== XSD_NS + 'dateTime' && dt !== XSD_NS + 'date' && dt !== XSD_NS + 'gYearMonth' && dt !== XSD_NS + 'gYear')
|
|
3894
|
+
return null;
|
|
3895
|
+
|
|
3896
|
+
const outLex = `"${v}${tz}"^^<${dt}>`;
|
|
3897
|
+
return internLiteral(outLex);
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3628
3900
|
function parseDatetimeLike(t) {
|
|
3629
3901
|
const d = parseXsdDateTerm(t);
|
|
3630
3902
|
if (d !== null) return d;
|
|
@@ -4920,6 +5192,74 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
4920
5192
|
return [];
|
|
4921
5193
|
}
|
|
4922
5194
|
|
|
5195
|
+
// -----------------------------------------------------------------
|
|
5196
|
+
// 4.3.1 tfn: Time Functions builtins
|
|
5197
|
+
// -----------------------------------------------------------------
|
|
5198
|
+
|
|
5199
|
+
// tfn:periodMinInclusive / periodMaxInclusive / periodMinExclusive / periodMaxExclusive
|
|
5200
|
+
// Schema: ( $s.1+ )+ tfn:* $o-
|
|
5201
|
+
const tfnPeriodKind =
|
|
5202
|
+
pv === TFN_NS + 'periodMinInclusive'
|
|
5203
|
+
? 'minInc'
|
|
5204
|
+
: pv === TFN_NS + 'periodMaxInclusive'
|
|
5205
|
+
? 'maxInc'
|
|
5206
|
+
: pv === TFN_NS + 'periodMinExclusive'
|
|
5207
|
+
? 'minEx'
|
|
5208
|
+
: pv === TFN_NS + 'periodMaxExclusive'
|
|
5209
|
+
? 'maxEx'
|
|
5210
|
+
: null;
|
|
5211
|
+
if (tfnPeriodKind) {
|
|
5212
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 1) return [];
|
|
5213
|
+
const arg = g.s.elems[0];
|
|
5214
|
+
const parts = __tfnParseTemporalLiteralParts(arg);
|
|
5215
|
+
if (!parts) return [];
|
|
5216
|
+
const bounds = __tfnComputePeriodBounds(parts);
|
|
5217
|
+
if (!bounds) return [];
|
|
5218
|
+
|
|
5219
|
+
let out;
|
|
5220
|
+
if (tfnPeriodKind === 'minInc') {
|
|
5221
|
+
out = __tfnMakeDateTimeLiteral({ ...bounds.startC }, bounds.tzMin);
|
|
5222
|
+
} else if (tfnPeriodKind === 'maxInc') {
|
|
5223
|
+
out = __tfnMakeDateTimeLiteral({ ...bounds.endC }, bounds.tzMax);
|
|
5224
|
+
} else if (tfnPeriodKind === 'minEx') {
|
|
5225
|
+
const c = __tfnSub1ms({ ...bounds.startC });
|
|
5226
|
+
out = __tfnMakeDateTimeLiteral(c, bounds.tzMin);
|
|
5227
|
+
} else {
|
|
5228
|
+
// maxEx
|
|
5229
|
+
const c = __tfnAdd1ms({ ...bounds.endC });
|
|
5230
|
+
out = __tfnMakeDateTimeLiteral(c, bounds.tzMax);
|
|
5231
|
+
}
|
|
5232
|
+
|
|
5233
|
+
if (g.o instanceof Var) {
|
|
5234
|
+
const s2 = { ...subst };
|
|
5235
|
+
s2[g.o.name] = out;
|
|
5236
|
+
return [s2];
|
|
5237
|
+
}
|
|
5238
|
+
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5239
|
+
|
|
5240
|
+
const s2 = unifyTerm(g.o, out, subst);
|
|
5241
|
+
return s2 !== null ? [s2] : [];
|
|
5242
|
+
}
|
|
5243
|
+
|
|
5244
|
+
// tfn:bindDefaultTimezone
|
|
5245
|
+
// Schema: ( $s.1+ $s.2+ )+ tfn:bindDefaultTimezone $o-
|
|
5246
|
+
if (pv === TFN_NS + 'bindDefaultTimezone') {
|
|
5247
|
+
if (!(g.s instanceof ListTerm) || g.s.elems.length !== 2) return [];
|
|
5248
|
+
const [timeLit, tzLit] = g.s.elems;
|
|
5249
|
+
const out = __tfnBindDefaultTimezone(timeLit, tzLit);
|
|
5250
|
+
if (!out) return [];
|
|
5251
|
+
|
|
5252
|
+
if (g.o instanceof Var) {
|
|
5253
|
+
const s2 = { ...subst };
|
|
5254
|
+
s2[g.o.name] = out;
|
|
5255
|
+
return [s2];
|
|
5256
|
+
}
|
|
5257
|
+
if (g.o instanceof Blank) return [{ ...subst }];
|
|
5258
|
+
|
|
5259
|
+
const s2 = unifyTerm(g.o, out, subst);
|
|
5260
|
+
return s2 !== null ? [s2] : [];
|
|
5261
|
+
}
|
|
5262
|
+
|
|
4923
5263
|
// -----------------------------------------------------------------
|
|
4924
5264
|
// 4.4 list: builtins
|
|
4925
5265
|
// -----------------------------------------------------------------
|
|
@@ -6144,6 +6484,7 @@ function isBuiltinPred(p) {
|
|
|
6144
6484
|
v.startsWith(LOG_NS) ||
|
|
6145
6485
|
v.startsWith(STRING_NS) ||
|
|
6146
6486
|
v.startsWith(TIME_NS) ||
|
|
6487
|
+
v.startsWith(TFN_NS) ||
|
|
6147
6488
|
v.startsWith(LIST_NS)
|
|
6148
6489
|
);
|
|
6149
6490
|
}
|
|
@@ -6568,7 +6909,7 @@ function forwardChain(facts, forwardRules, backRules, onDerived /* optional */)
|
|
|
6568
6909
|
if (allKnown) continue;
|
|
6569
6910
|
}
|
|
6570
6911
|
|
|
6571
|
-
const maxSols =
|
|
6912
|
+
const maxSols = r.isFuse || headIsStrictGround ? 1 : undefined;
|
|
6572
6913
|
const sols = proveGoals(r.premise.slice(), empty, facts, backRules, 0, visited, varGen, maxSols);
|
|
6573
6914
|
|
|
6574
6915
|
// Inference fuse
|
|
@@ -7102,7 +7443,8 @@ function main() {
|
|
|
7102
7443
|
` -s, --super-restricted Disable all builtins except => and <=.\n` +
|
|
7103
7444
|
` -a, --ast Print parsed AST as JSON and exit.\n` +
|
|
7104
7445
|
` --strings Print log:outputString strings (ordered by key) instead of N3 output.\n` +
|
|
7105
|
-
` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n
|
|
7446
|
+
` --enforce-https Rewrite http:// IRIs to https:// for log dereferencing builtins.\n` +
|
|
7447
|
+
` --stream Stream derived triples as soon as they are derived.\n`;
|
|
7106
7448
|
(toStderr ? console.error : console.log)(msg);
|
|
7107
7449
|
}
|
|
7108
7450
|
|
|
@@ -7124,6 +7466,7 @@ function main() {
|
|
|
7124
7466
|
const showAst = argv.includes('--ast') || argv.includes('-a');
|
|
7125
7467
|
|
|
7126
7468
|
const outputStringsMode = argv.includes('--strings');
|
|
7469
|
+
const streamMode = argv.includes('--stream');
|
|
7127
7470
|
|
|
7128
7471
|
// --enforce-https: rewrite http:// -> https:// for log dereferencing builtins
|
|
7129
7472
|
if (argv.includes('--enforce-https')) {
|
|
@@ -7206,13 +7549,133 @@ function main() {
|
|
|
7206
7549
|
materializeRdfLists(triples, frules, brules);
|
|
7207
7550
|
|
|
7208
7551
|
const facts = triples.filter((tr) => isGroundTriple(tr));
|
|
7209
|
-
|
|
7552
|
+
|
|
7210
7553
|
// If requested, print log:outputString values (ordered by subject key) and exit.
|
|
7554
|
+
// Note: log:outputString values may depend on derived facts, so we must saturate first.
|
|
7211
7555
|
if (outputStringsMode) {
|
|
7556
|
+
forwardChain(facts, frules, brules);
|
|
7212
7557
|
const out = __collectOutputStringsFromFacts(facts, prefixes);
|
|
7213
7558
|
if (out) process.stdout.write(out);
|
|
7214
7559
|
process.exit(0);
|
|
7215
7560
|
}
|
|
7561
|
+
|
|
7562
|
+
// In --stream mode we print prefixes *before* any derivations happen.
|
|
7563
|
+
// To keep the header small and stable, emit only prefixes that are actually
|
|
7564
|
+
// used (as QNames) in the *input* N3 program.
|
|
7565
|
+
function prefixesUsedInInputTokens(toks2, prefEnv) {
|
|
7566
|
+
const used = new Set();
|
|
7567
|
+
|
|
7568
|
+
function maybeAddFromQName(name) {
|
|
7569
|
+
if (typeof name !== 'string') return;
|
|
7570
|
+
if (!name.includes(':')) return;
|
|
7571
|
+
if (name.startsWith('_:')) return; // blank node
|
|
7572
|
+
|
|
7573
|
+
// Split only on the first ':'
|
|
7574
|
+
const idx = name.indexOf(':');
|
|
7575
|
+
const p = name.slice(0, idx); // may be '' for ":foo"
|
|
7576
|
+
|
|
7577
|
+
// Ignore things like "http://..." unless that prefix is actually defined.
|
|
7578
|
+
if (!Object.prototype.hasOwnProperty.call(prefEnv.map, p)) return;
|
|
7579
|
+
|
|
7580
|
+
used.add(p);
|
|
7581
|
+
}
|
|
7582
|
+
|
|
7583
|
+
for (let i = 0; i < toks2.length; i++) {
|
|
7584
|
+
const t = toks2[i];
|
|
7585
|
+
|
|
7586
|
+
// Skip @prefix ... .
|
|
7587
|
+
if (t.typ === 'AtPrefix') {
|
|
7588
|
+
// @prefix <pfx:> <iri> .
|
|
7589
|
+
// We skip the directive itself so declared-but-unused prefixes don't count.
|
|
7590
|
+
// Advance until we pass the terminating dot (if present).
|
|
7591
|
+
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
7592
|
+
continue;
|
|
7593
|
+
}
|
|
7594
|
+
// Skip @base ... .
|
|
7595
|
+
if (t.typ === 'AtBase') {
|
|
7596
|
+
while (i < toks2.length && toks2[i].typ !== 'Dot' && toks2[i].typ !== 'EOF') i++;
|
|
7597
|
+
continue;
|
|
7598
|
+
}
|
|
7599
|
+
|
|
7600
|
+
// Skip SPARQL/Turtle PREFIX pfx: <iri>
|
|
7601
|
+
if (
|
|
7602
|
+
t.typ === 'Ident' &&
|
|
7603
|
+
typeof t.value === 'string' &&
|
|
7604
|
+
t.value.toLowerCase() === 'prefix' &&
|
|
7605
|
+
toks2[i + 1] &&
|
|
7606
|
+
toks2[i + 1].typ === 'Ident' &&
|
|
7607
|
+
typeof toks2[i + 1].value === 'string' &&
|
|
7608
|
+
toks2[i + 1].value.endsWith(':') &&
|
|
7609
|
+
toks2[i + 2] &&
|
|
7610
|
+
(toks2[i + 2].typ === 'IriRef' || toks2[i + 2].typ === 'Ident')
|
|
7611
|
+
) {
|
|
7612
|
+
// Consume PREFIX <pfx:> <iri>
|
|
7613
|
+
i += 2;
|
|
7614
|
+
continue;
|
|
7615
|
+
}
|
|
7616
|
+
// Skip SPARQL BASE <iri>
|
|
7617
|
+
if (
|
|
7618
|
+
t.typ === 'Ident' &&
|
|
7619
|
+
typeof t.value === 'string' &&
|
|
7620
|
+
t.value.toLowerCase() === 'base' &&
|
|
7621
|
+
toks2[i + 1] &&
|
|
7622
|
+
toks2[i + 1].typ === 'IriRef'
|
|
7623
|
+
) {
|
|
7624
|
+
i += 1;
|
|
7625
|
+
continue;
|
|
7626
|
+
}
|
|
7627
|
+
|
|
7628
|
+
// Count QNames in identifiers (including datatypes like xsd:integer).
|
|
7629
|
+
if (t.typ === 'Ident') {
|
|
7630
|
+
maybeAddFromQName(t.value);
|
|
7631
|
+
}
|
|
7632
|
+
}
|
|
7633
|
+
|
|
7634
|
+
return used;
|
|
7635
|
+
}
|
|
7636
|
+
|
|
7637
|
+
function restrictPrefixEnv(prefEnv, usedSet) {
|
|
7638
|
+
const m = {};
|
|
7639
|
+
for (const p of usedSet) {
|
|
7640
|
+
if (Object.prototype.hasOwnProperty.call(prefEnv.map, p)) {
|
|
7641
|
+
m[p] = prefEnv.map[p];
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
return new PrefixEnv(m, prefEnv.baseIri || '');
|
|
7645
|
+
}
|
|
7646
|
+
|
|
7647
|
+
// Streaming mode: print (input) prefixes first, then print derived triples as soon as they are found.
|
|
7648
|
+
if (streamMode) {
|
|
7649
|
+
const usedInInput = prefixesUsedInInputTokens(toks, prefixes);
|
|
7650
|
+
const outPrefixes = restrictPrefixEnv(prefixes, usedInInput);
|
|
7651
|
+
|
|
7652
|
+
// Ensure log:trace uses the same compact prefix set as the output.
|
|
7653
|
+
__tracePrefixes = outPrefixes;
|
|
7654
|
+
|
|
7655
|
+
const entries = Object.entries(outPrefixes.map)
|
|
7656
|
+
.filter(([_p, base]) => !!base)
|
|
7657
|
+
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
|
|
7658
|
+
|
|
7659
|
+
for (const [pfx, base] of entries) {
|
|
7660
|
+
if (pfx === '') console.log(`@prefix : <${base}> .`);
|
|
7661
|
+
else console.log(`@prefix ${pfx}: <${base}> .`);
|
|
7662
|
+
}
|
|
7663
|
+
if (entries.length) console.log();
|
|
7664
|
+
|
|
7665
|
+
forwardChain(facts, frules, brules, (df) => {
|
|
7666
|
+
if (proofCommentsEnabled) {
|
|
7667
|
+
printExplanation(df, outPrefixes);
|
|
7668
|
+
console.log(tripleToN3(df.fact, outPrefixes));
|
|
7669
|
+
console.log();
|
|
7670
|
+
} else {
|
|
7671
|
+
console.log(tripleToN3(df.fact, outPrefixes));
|
|
7672
|
+
}
|
|
7673
|
+
});
|
|
7674
|
+
return;
|
|
7675
|
+
}
|
|
7676
|
+
|
|
7677
|
+
// Default (non-streaming): derive everything first, then print only the newly derived facts.
|
|
7678
|
+
const derived = forwardChain(facts, frules, brules);
|
|
7216
7679
|
const derivedTriples = derived.map((df) => df.fact);
|
|
7217
7680
|
const usedPrefixes = prefixes.prefixesUsedForOutput(derivedTriples);
|
|
7218
7681
|
|
package/index.js
CHANGED
|
@@ -22,7 +22,11 @@ function reason(opt = {}, n3_input = '') {
|
|
|
22
22
|
const proofCommentsSpecified = typeof opt.proofComments === 'boolean' || typeof opt.noProofComments === 'boolean';
|
|
23
23
|
|
|
24
24
|
const proofComments =
|
|
25
|
-
typeof opt.proofComments === 'boolean'
|
|
25
|
+
typeof opt.proofComments === 'boolean'
|
|
26
|
+
? opt.proofComments
|
|
27
|
+
: typeof opt.noProofComments === 'boolean'
|
|
28
|
+
? !opt.noProofComments
|
|
29
|
+
: false;
|
|
26
30
|
|
|
27
31
|
// Only pass a flag when the caller explicitly asked.
|
|
28
32
|
// (CLI default is now: no proof comments.)
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
const assert = require('node:assert/strict');
|
|
4
4
|
const { reason } = require('..');
|
|
5
|
+
// Direct eyeling.js API (in-process) for testing reasonStream/onDerived.
|
|
6
|
+
// This is the "latest eyeling.js" surface and is used by the browser demo.
|
|
7
|
+
const { reasonStream } = require('../eyeling.js');
|
|
5
8
|
|
|
6
9
|
const TTY = process.stdout.isTTY;
|
|
7
10
|
const C = TTY ? { g: '\x1b[32m', r: '\x1b[31m', y: '\x1b[33m', dim: '\x1b[2m', n: '\x1b[0m' } : { g: '', r: '', y: '', dim: '', n: '' };
|
|
@@ -687,6 +690,104 @@ _:l2 rdf:rest rdf:nil.
|
|
|
687
690
|
new RegExp(`${EX}s>\\s+<${EX}q3>\\s+<${EX}b>\\s*\\.`),
|
|
688
691
|
],
|
|
689
692
|
},
|
|
693
|
+
|
|
694
|
+
// -------------------------
|
|
695
|
+
// Newer eyeling.js features
|
|
696
|
+
// -------------------------
|
|
697
|
+
|
|
698
|
+
{
|
|
699
|
+
name: '51 --strings: prints log:outputString values ordered by key (subject)',
|
|
700
|
+
opt: ['--strings', '-n'],
|
|
701
|
+
input: `@prefix log: <http://www.w3.org/2000/10/swap/log#>.
|
|
702
|
+
|
|
703
|
+
<http://example.org/2> log:outputString "B".
|
|
704
|
+
<http://example.org/1> log:outputString "A".
|
|
705
|
+
`,
|
|
706
|
+
// CLI prints concatenated strings and exits.
|
|
707
|
+
check(out) {
|
|
708
|
+
assert.equal(String(out).trimEnd(), 'AB');
|
|
709
|
+
},
|
|
710
|
+
},
|
|
711
|
+
|
|
712
|
+
{
|
|
713
|
+
name: '52 --ast: prints parse result as JSON array [prefixes, triples, frules, brules]',
|
|
714
|
+
opt: ['--ast'],
|
|
715
|
+
input: `@prefix ex: <http://example.org/>.
|
|
716
|
+
ex:s ex:p ex:o.
|
|
717
|
+
`,
|
|
718
|
+
expect: [/^\s*\[/m],
|
|
719
|
+
check(out) {
|
|
720
|
+
const v = JSON.parse(String(out));
|
|
721
|
+
assert.ok(Array.isArray(v), 'AST output should be a JSON array');
|
|
722
|
+
assert.equal(v.length, 4, 'AST output should have 4 top-level elements');
|
|
723
|
+
// The second element is the parsed triples array.
|
|
724
|
+
assert.ok(Array.isArray(v[1]), 'AST[1] (triples) should be an array');
|
|
725
|
+
},
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
{
|
|
729
|
+
name: '53 --stream: prints prefixes used in input (not just derived output) before streaming triples',
|
|
730
|
+
opt: ['--stream', '-n'],
|
|
731
|
+
input: `@prefix ex: <http://example.org/>.
|
|
732
|
+
@prefix p: <http://premise.example/>.
|
|
733
|
+
@prefix unused: <http://unused.example/>.
|
|
734
|
+
|
|
735
|
+
ex:a p:trig ex:b.
|
|
736
|
+
{ ?s p:trig ?o. } => { ?s ex:q ?o. }.
|
|
737
|
+
`,
|
|
738
|
+
expect: [
|
|
739
|
+
/@prefix\s+ex:\s+<http:\/\/example\.org\/>\s*\./m,
|
|
740
|
+
/@prefix\s+p:\s+<http:\/\/premise\.example\/>\s*\./m,
|
|
741
|
+
/(?:ex:a|<http:\/\/example\.org\/a>)\s+(?:ex:q|<http:\/\/example\.org\/q>)\s+(?:ex:b|<http:\/\/example\.org\/b>)\s*\./m,
|
|
742
|
+
],
|
|
743
|
+
notExpect: [/@prefix\s+unused:/m, /^#/m],
|
|
744
|
+
check(out) {
|
|
745
|
+
const lines = String(out).split(/\r?\n/);
|
|
746
|
+
const firstNonPrefix = lines.findIndex((l) => {
|
|
747
|
+
const t = l.trim();
|
|
748
|
+
return t && !t.startsWith('@prefix');
|
|
749
|
+
});
|
|
750
|
+
assert.ok(firstNonPrefix > 0, 'Expected at least one @prefix line before the first triple');
|
|
751
|
+
for (let i = 0; i < firstNonPrefix; i++) {
|
|
752
|
+
const t = lines[i].trim();
|
|
753
|
+
if (!t) continue;
|
|
754
|
+
assert.ok(t.startsWith('@prefix'), `Non-prefix line found before first triple: ${lines[i]}`);
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
{
|
|
760
|
+
name: '54 reasonStream: onDerived callback fires and includeInputFactsInClosure=false excludes input facts',
|
|
761
|
+
run() {
|
|
762
|
+
const input = `
|
|
763
|
+
{ <http://example.org/s> <http://example.org/p> <http://example.org/o>. }
|
|
764
|
+
=> { <http://example.org/s> <http://example.org/q> <http://example.org/o>. }.
|
|
765
|
+
|
|
766
|
+
<http://example.org/s> <http://example.org/p> <http://example.org/o>.
|
|
767
|
+
`;
|
|
768
|
+
|
|
769
|
+
const seen = [];
|
|
770
|
+
const r = reasonStream(input, {
|
|
771
|
+
proof: false,
|
|
772
|
+
includeInputFactsInClosure: false,
|
|
773
|
+
onDerived: ({ triple }) => seen.push(triple),
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// stash for check()
|
|
777
|
+
this._seen = seen;
|
|
778
|
+
this._result = r;
|
|
779
|
+
return r.closureN3;
|
|
780
|
+
},
|
|
781
|
+
expect: [/http:\/\/example\.org\/q/m],
|
|
782
|
+
notExpect: [/http:\/\/example\.org\/p/m],
|
|
783
|
+
check(out, tc) {
|
|
784
|
+
assert.equal(tc._seen.length, 1, 'Expected onDerived to be called once');
|
|
785
|
+
assert.match(tc._seen[0], /http:\/\/example\.org\/q/, 'Expected streamed triple to be the derived one');
|
|
786
|
+
// closureN3 should be exactly the derived triple (no input facts).
|
|
787
|
+
assert.ok(String(out).trim().includes('http://example.org/q'));
|
|
788
|
+
assert.ok(!String(out).includes('http://example.org/p'));
|
|
789
|
+
},
|
|
790
|
+
},
|
|
690
791
|
];
|
|
691
792
|
|
|
692
793
|
let passed = 0;
|
|
@@ -699,7 +800,7 @@ let failed = 0;
|
|
|
699
800
|
for (const tc of cases) {
|
|
700
801
|
const start = msNow();
|
|
701
802
|
try {
|
|
702
|
-
const out = reason(tc.opt, tc.input);
|
|
803
|
+
const out = typeof tc.run === 'function' ? await tc.run() : reason(tc.opt, tc.input);
|
|
703
804
|
|
|
704
805
|
if (tc.expectErrorCode != null || tc.expectError) {
|
|
705
806
|
throw new Error(`Expected an error, but reason() returned output:\n${out}`);
|
|
@@ -708,7 +809,7 @@ let failed = 0;
|
|
|
708
809
|
for (const re of tc.expect || []) mustMatch(out, re, `${tc.name}: missing expected pattern ${re}`);
|
|
709
810
|
for (const re of tc.notExpect || []) mustNotMatch(out, re, `${tc.name}: unexpected pattern ${re}`);
|
|
710
811
|
|
|
711
|
-
if (typeof tc.check === 'function') tc.check(out);
|
|
812
|
+
if (typeof tc.check === 'function') tc.check(out, tc);
|
|
712
813
|
|
|
713
814
|
const dur = msNow() - start;
|
|
714
815
|
ok(`${tc.name} ${C.dim}(${dur} ms)${C.n}`);
|