eyeling 1.24.23 → 1.24.25
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/HANDBOOK.md +1 -1
- package/dist/browser/eyeling.browser.js +244 -17
- package/eyeling.js +244 -17
- package/lib/builtins.js +106 -10
- package/lib/engine.js +4 -1
- package/lib/prelude.js +134 -6
- package/package.json +1 -1
- package/test/api.test.js +29 -0
- package/test/playground.test.js +41 -8
package/HANDBOOK.md
CHANGED
|
@@ -3733,7 +3733,7 @@ The output behavior also adapts to the kind of N3 program being run. In some cas
|
|
|
3733
3733
|
|
|
3734
3734
|
For Markdown-oriented `log:outputString` examples, the output pane has two views: a rendered Markdown view and a Markdown source view. Those tabs appear only when the actual output looks like Markdown; Turtle or other plain output stays in the source editor without the Markdown toggle. The rendered view is selected by default for Markdown output, while the source view keeps the exact generated Markdown available for copying, inspection, or comparison.
|
|
3735
3735
|
|
|
3736
|
-
Repository example reports often contain relative source links that are written for the checked-in files under `examples/output/*.md`. When such an example is loaded into the playground from a raw URL, the rendered Markdown view resolves those relative links against the corresponding static output page rather than against `/playground`, so links like `../name.n3` and `../input/name.trig` continue to point to the intended example files.
|
|
3736
|
+
Repository example reports often contain relative source links that are written for the checked-in files under `examples/output/*.md`. When such an example is loaded into the playground from a raw URL, or restored later from compact/Gist-backed state, the rendered Markdown view resolves those relative links against the corresponding static output page rather than against `/playground`, so links like `../name.n3` and `../input/name.trig` continue to point to the intended example files. The playground preserves this base in shared state when possible and can also recover it from the injected `@base <.../examples/name.n3>` line.
|
|
3737
3737
|
|
|
3738
3738
|
### I.4 Error handling and explainability
|
|
3739
3739
|
|
|
@@ -1691,15 +1691,85 @@ function __listElemsForBuiltin(listLike, facts) {
|
|
|
1691
1691
|
return null;
|
|
1692
1692
|
}
|
|
1693
1693
|
|
|
1694
|
-
function
|
|
1695
|
-
if (
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
const s2 = unifyTerm(oTerm, first, subst);
|
|
1699
|
-
return s2 !== null ? [s2] : [];
|
|
1694
|
+
function __pushBuiltinDeltaLimited(out, delta, maxResults) {
|
|
1695
|
+
if (delta === null) return false;
|
|
1696
|
+
out.push(delta);
|
|
1697
|
+
return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
|
|
1700
1698
|
}
|
|
1701
1699
|
|
|
1702
|
-
function
|
|
1700
|
+
function __collectListLikeTermsFromTerm(t, out, seen) {
|
|
1701
|
+
if (t instanceof ListTerm || t instanceof OpenListTerm) {
|
|
1702
|
+
const k = termFastKey ? termFastKey(t) : null;
|
|
1703
|
+
if (k === null || !seen.has(k)) {
|
|
1704
|
+
if (k !== null) seen.add(k);
|
|
1705
|
+
out.push(t);
|
|
1706
|
+
}
|
|
1707
|
+
if (t instanceof ListTerm) {
|
|
1708
|
+
for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1709
|
+
} else {
|
|
1710
|
+
for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (t instanceof GraphTerm) {
|
|
1715
|
+
for (const tr of t.triples) {
|
|
1716
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1717
|
+
__collectListLikeTermsFromTerm(tr.p, out, seen);
|
|
1718
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function __collectListLikeTermsFromFacts(facts) {
|
|
1724
|
+
const out = [];
|
|
1725
|
+
const seen = new Set();
|
|
1726
|
+
for (const tr of facts) {
|
|
1727
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1728
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1729
|
+
}
|
|
1730
|
+
return out;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1734
|
+
if (sTerm instanceof ListTerm) {
|
|
1735
|
+
if (!sTerm.elems.length) return [];
|
|
1736
|
+
const first = sTerm.elems[0];
|
|
1737
|
+
const s2 = unifyTerm(oTerm, first, subst);
|
|
1738
|
+
return s2 !== null ? [s2] : [];
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// For a variable subject, enumerate already-existing collection terms.
|
|
1742
|
+
// This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
|
|
1743
|
+
// bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
|
|
1744
|
+
// Also include ordinary rdf:first facts so the builtin path does not hide
|
|
1745
|
+
// RDF-serialized lists when the subject starts unbound.
|
|
1746
|
+
if (sTerm instanceof Var) {
|
|
1747
|
+
const out = [];
|
|
1748
|
+
|
|
1749
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1750
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1751
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1752
|
+
if (s2 === null) continue;
|
|
1753
|
+
s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
|
|
1754
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const RDF_FIRST = RDF_NS + 'first';
|
|
1758
|
+
for (const tr of facts) {
|
|
1759
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
|
|
1760
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1761
|
+
if (s2 === null) continue;
|
|
1762
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1763
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return out;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return [];
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1703
1773
|
// Closed list: (a b c) -> (b c)
|
|
1704
1774
|
if (sTerm instanceof ListTerm) {
|
|
1705
1775
|
if (!sTerm.elems.length) return [];
|
|
@@ -1720,6 +1790,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
|
1720
1790
|
return s2 !== null ? [s2] : [];
|
|
1721
1791
|
}
|
|
1722
1792
|
|
|
1793
|
+
// See evalListFirstLikeBuiltin(): if the collection subject is still a
|
|
1794
|
+
// variable, enumerate known list literals and ordinary rdf:rest facts.
|
|
1795
|
+
if (sTerm instanceof Var) {
|
|
1796
|
+
const out = [];
|
|
1797
|
+
|
|
1798
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1799
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1800
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1801
|
+
if (s2 === null) continue;
|
|
1802
|
+
const rest = new ListTerm(listTerm.elems.slice(1));
|
|
1803
|
+
s2 = unifyTerm(oTerm, rest, s2);
|
|
1804
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const RDF_REST = RDF_NS + 'rest';
|
|
1808
|
+
for (const tr of facts) {
|
|
1809
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
|
|
1810
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1811
|
+
if (s2 === null) continue;
|
|
1812
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1813
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return out;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1723
1819
|
return [];
|
|
1724
1820
|
}
|
|
1725
1821
|
|
|
@@ -2859,14 +2955,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2859
2955
|
return s2 !== null ? [s2] : [];
|
|
2860
2956
|
}
|
|
2861
2957
|
if (pv === RDF_NS + 'first') {
|
|
2862
|
-
return evalListFirstLikeBuiltin(g.s, g.o, subst);
|
|
2958
|
+
return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2863
2959
|
}
|
|
2864
2960
|
|
|
2865
2961
|
// list:rest and rdf:rest
|
|
2866
2962
|
// true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
|
|
2867
2963
|
// Schema: $s+ list:rest $o-
|
|
2868
2964
|
if (pv === LIST_NS + 'rest') {
|
|
2869
|
-
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2965
|
+
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2870
2966
|
const xs = __listElemsForBuiltin(g.s, facts);
|
|
2871
2967
|
if (!xs || !xs.length) return [];
|
|
2872
2968
|
const rest = new ListTerm(xs.slice(1));
|
|
@@ -2874,7 +2970,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2874
2970
|
return s2 !== null ? [s2] : [];
|
|
2875
2971
|
}
|
|
2876
2972
|
if (pv === RDF_NS + 'rest') {
|
|
2877
|
-
return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2973
|
+
return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2878
2974
|
}
|
|
2879
2975
|
|
|
2880
2976
|
// list:iterate
|
|
@@ -7918,7 +8014,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
7918
8014
|
const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
|
|
7919
8015
|
const shouldTreatAsBuiltin =
|
|
7920
8016
|
isBuiltinPred(goal0.p) &&
|
|
7921
|
-
!(
|
|
8017
|
+
!(
|
|
8018
|
+
isRdfFirstOrRest &&
|
|
8019
|
+
!(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
|
|
8020
|
+
);
|
|
7922
8021
|
|
|
7923
8022
|
if (shouldTreatAsBuiltin) {
|
|
7924
8023
|
const remaining = max - results.length;
|
|
@@ -11703,14 +11802,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
|
|
|
11703
11802
|
const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
|
|
11704
11803
|
const RDF_JSON_DT = RDF_NS + 'JSON';
|
|
11705
11804
|
|
|
11805
|
+
function parseUriReferenceForResolution(uri) {
|
|
11806
|
+
// RFC 3986 Appendix B-style component parser, with the scheme tightened to
|
|
11807
|
+
// the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
|
|
11808
|
+
// empty query is defined, while no `?` means undefined.
|
|
11809
|
+
const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
|
|
11810
|
+
if (!m) return null;
|
|
11811
|
+
return {
|
|
11812
|
+
scheme: m[2] !== undefined ? m[2] : undefined,
|
|
11813
|
+
authority: m[4] !== undefined ? m[4] : undefined,
|
|
11814
|
+
path: m[5] || '',
|
|
11815
|
+
query: m[6] !== undefined ? (m[7] || '') : undefined,
|
|
11816
|
+
fragment: m[8] !== undefined ? (m[9] || '') : undefined,
|
|
11817
|
+
};
|
|
11818
|
+
}
|
|
11819
|
+
|
|
11820
|
+
function recomposeUriReference(parts) {
|
|
11821
|
+
let out = '';
|
|
11822
|
+
if (parts.scheme !== undefined) out += `${parts.scheme}:`;
|
|
11823
|
+
if (parts.authority !== undefined) out += `//${parts.authority}`;
|
|
11824
|
+
out += parts.path || '';
|
|
11825
|
+
if (parts.query !== undefined) out += `?${parts.query}`;
|
|
11826
|
+
if (parts.fragment !== undefined) out += `#${parts.fragment}`;
|
|
11827
|
+
return out;
|
|
11828
|
+
}
|
|
11829
|
+
|
|
11830
|
+
function removeLastPathSegment(path) {
|
|
11831
|
+
if (!path) return '';
|
|
11832
|
+
const i = path.lastIndexOf('/');
|
|
11833
|
+
if (i < 0) return '';
|
|
11834
|
+
if (i === 0) return '';
|
|
11835
|
+
return path.slice(0, i);
|
|
11836
|
+
}
|
|
11837
|
+
|
|
11838
|
+
function removeDotSegments(path) {
|
|
11839
|
+
// RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
|
|
11840
|
+
// Eyeling preserves IRI spelling (for example, it does not add a trailing
|
|
11841
|
+
// slash to `http://example.org`) while still normalizing `.` and `..` path
|
|
11842
|
+
// segments as required by section 5.2.2.
|
|
11843
|
+
let input = String(path || '');
|
|
11844
|
+
let output = '';
|
|
11845
|
+
|
|
11846
|
+
while (input.length > 0) {
|
|
11847
|
+
if (input.startsWith('../')) {
|
|
11848
|
+
input = input.slice(3);
|
|
11849
|
+
} else if (input.startsWith('./')) {
|
|
11850
|
+
input = input.slice(2);
|
|
11851
|
+
} else if (input.startsWith('/./')) {
|
|
11852
|
+
input = `/${input.slice(3)}`;
|
|
11853
|
+
} else if (input === '/.') {
|
|
11854
|
+
input = '/';
|
|
11855
|
+
} else if (input.startsWith('/../')) {
|
|
11856
|
+
input = `/${input.slice(4)}`;
|
|
11857
|
+
output = removeLastPathSegment(output);
|
|
11858
|
+
} else if (input === '/..') {
|
|
11859
|
+
input = '/';
|
|
11860
|
+
output = removeLastPathSegment(output);
|
|
11861
|
+
} else if (input === '.' || input === '..') {
|
|
11862
|
+
input = '';
|
|
11863
|
+
} else {
|
|
11864
|
+
let segmentEnd;
|
|
11865
|
+
if (input[0] === '/') {
|
|
11866
|
+
segmentEnd = input.indexOf('/', 1);
|
|
11867
|
+
} else {
|
|
11868
|
+
segmentEnd = input.indexOf('/');
|
|
11869
|
+
}
|
|
11870
|
+
|
|
11871
|
+
if (segmentEnd < 0) {
|
|
11872
|
+
output += input;
|
|
11873
|
+
input = '';
|
|
11874
|
+
} else {
|
|
11875
|
+
output += input.slice(0, segmentEnd);
|
|
11876
|
+
input = input.slice(segmentEnd);
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
|
|
11881
|
+
return output;
|
|
11882
|
+
}
|
|
11883
|
+
|
|
11884
|
+
function mergePaths(base, refPath) {
|
|
11885
|
+
if (base.authority !== undefined && base.path === '') {
|
|
11886
|
+
return `/${refPath}`;
|
|
11887
|
+
}
|
|
11888
|
+
const i = base.path.lastIndexOf('/');
|
|
11889
|
+
if (i < 0) return refPath;
|
|
11890
|
+
return `${base.path.slice(0, i + 1)}${refPath}`;
|
|
11891
|
+
}
|
|
11892
|
+
|
|
11706
11893
|
function resolveIriRef(ref, base) {
|
|
11707
|
-
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11894
|
+
const r = parseUriReferenceForResolution(ref);
|
|
11895
|
+
if (!r) return ref;
|
|
11896
|
+
|
|
11897
|
+
const baseParts = base ? parseUriReferenceForResolution(base) : null;
|
|
11898
|
+
|
|
11899
|
+
// Absolute references do not need a base, but RFC 3986 section 5.2.2 still
|
|
11900
|
+
// applies remove_dot_segments(R.path) when R.scheme is defined.
|
|
11901
|
+
if (r.scheme !== undefined) {
|
|
11902
|
+
return recomposeUriReference({
|
|
11903
|
+
scheme: r.scheme,
|
|
11904
|
+
authority: r.authority,
|
|
11905
|
+
path: removeDotSegments(r.path),
|
|
11906
|
+
query: r.query,
|
|
11907
|
+
fragment: r.fragment,
|
|
11908
|
+
});
|
|
11713
11909
|
}
|
|
11910
|
+
|
|
11911
|
+
// Without a usable base, preserve relative references as written.
|
|
11912
|
+
if (!baseParts || baseParts.scheme === undefined) return ref;
|
|
11913
|
+
|
|
11914
|
+
const t = {
|
|
11915
|
+
scheme: baseParts.scheme,
|
|
11916
|
+
authority: undefined,
|
|
11917
|
+
path: '',
|
|
11918
|
+
query: undefined,
|
|
11919
|
+
fragment: r.fragment,
|
|
11920
|
+
};
|
|
11921
|
+
|
|
11922
|
+
if (r.authority !== undefined) {
|
|
11923
|
+
t.authority = r.authority;
|
|
11924
|
+
t.path = removeDotSegments(r.path);
|
|
11925
|
+
t.query = r.query;
|
|
11926
|
+
} else if (r.path === '') {
|
|
11927
|
+
t.authority = baseParts.authority;
|
|
11928
|
+
t.path = baseParts.path;
|
|
11929
|
+
t.query = r.query !== undefined ? r.query : baseParts.query;
|
|
11930
|
+
} else {
|
|
11931
|
+
t.authority = baseParts.authority;
|
|
11932
|
+
if (r.path.startsWith('/')) {
|
|
11933
|
+
t.path = removeDotSegments(r.path);
|
|
11934
|
+
} else {
|
|
11935
|
+
t.path = removeDotSegments(mergePaths(baseParts, r.path));
|
|
11936
|
+
}
|
|
11937
|
+
t.query = r.query;
|
|
11938
|
+
}
|
|
11939
|
+
|
|
11940
|
+
return recomposeUriReference(t);
|
|
11714
11941
|
}
|
|
11715
11942
|
|
|
11716
11943
|
// -----------------------------------------------------------------------------
|
package/eyeling.js
CHANGED
|
@@ -1691,15 +1691,85 @@ function __listElemsForBuiltin(listLike, facts) {
|
|
|
1691
1691
|
return null;
|
|
1692
1692
|
}
|
|
1693
1693
|
|
|
1694
|
-
function
|
|
1695
|
-
if (
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
const s2 = unifyTerm(oTerm, first, subst);
|
|
1699
|
-
return s2 !== null ? [s2] : [];
|
|
1694
|
+
function __pushBuiltinDeltaLimited(out, delta, maxResults) {
|
|
1695
|
+
if (delta === null) return false;
|
|
1696
|
+
out.push(delta);
|
|
1697
|
+
return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
|
|
1700
1698
|
}
|
|
1701
1699
|
|
|
1702
|
-
function
|
|
1700
|
+
function __collectListLikeTermsFromTerm(t, out, seen) {
|
|
1701
|
+
if (t instanceof ListTerm || t instanceof OpenListTerm) {
|
|
1702
|
+
const k = termFastKey ? termFastKey(t) : null;
|
|
1703
|
+
if (k === null || !seen.has(k)) {
|
|
1704
|
+
if (k !== null) seen.add(k);
|
|
1705
|
+
out.push(t);
|
|
1706
|
+
}
|
|
1707
|
+
if (t instanceof ListTerm) {
|
|
1708
|
+
for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1709
|
+
} else {
|
|
1710
|
+
for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (t instanceof GraphTerm) {
|
|
1715
|
+
for (const tr of t.triples) {
|
|
1716
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1717
|
+
__collectListLikeTermsFromTerm(tr.p, out, seen);
|
|
1718
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
function __collectListLikeTermsFromFacts(facts) {
|
|
1724
|
+
const out = [];
|
|
1725
|
+
const seen = new Set();
|
|
1726
|
+
for (const tr of facts) {
|
|
1727
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1728
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1729
|
+
}
|
|
1730
|
+
return out;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1734
|
+
if (sTerm instanceof ListTerm) {
|
|
1735
|
+
if (!sTerm.elems.length) return [];
|
|
1736
|
+
const first = sTerm.elems[0];
|
|
1737
|
+
const s2 = unifyTerm(oTerm, first, subst);
|
|
1738
|
+
return s2 !== null ? [s2] : [];
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
// For a variable subject, enumerate already-existing collection terms.
|
|
1742
|
+
// This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
|
|
1743
|
+
// bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
|
|
1744
|
+
// Also include ordinary rdf:first facts so the builtin path does not hide
|
|
1745
|
+
// RDF-serialized lists when the subject starts unbound.
|
|
1746
|
+
if (sTerm instanceof Var) {
|
|
1747
|
+
const out = [];
|
|
1748
|
+
|
|
1749
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1750
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1751
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1752
|
+
if (s2 === null) continue;
|
|
1753
|
+
s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
|
|
1754
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const RDF_FIRST = RDF_NS + 'first';
|
|
1758
|
+
for (const tr of facts) {
|
|
1759
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
|
|
1760
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1761
|
+
if (s2 === null) continue;
|
|
1762
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1763
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
return out;
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
return [];
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1703
1773
|
// Closed list: (a b c) -> (b c)
|
|
1704
1774
|
if (sTerm instanceof ListTerm) {
|
|
1705
1775
|
if (!sTerm.elems.length) return [];
|
|
@@ -1720,6 +1790,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
|
1720
1790
|
return s2 !== null ? [s2] : [];
|
|
1721
1791
|
}
|
|
1722
1792
|
|
|
1793
|
+
// See evalListFirstLikeBuiltin(): if the collection subject is still a
|
|
1794
|
+
// variable, enumerate known list literals and ordinary rdf:rest facts.
|
|
1795
|
+
if (sTerm instanceof Var) {
|
|
1796
|
+
const out = [];
|
|
1797
|
+
|
|
1798
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1799
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1800
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1801
|
+
if (s2 === null) continue;
|
|
1802
|
+
const rest = new ListTerm(listTerm.elems.slice(1));
|
|
1803
|
+
s2 = unifyTerm(oTerm, rest, s2);
|
|
1804
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const RDF_REST = RDF_NS + 'rest';
|
|
1808
|
+
for (const tr of facts) {
|
|
1809
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
|
|
1810
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1811
|
+
if (s2 === null) continue;
|
|
1812
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1813
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return out;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1723
1819
|
return [];
|
|
1724
1820
|
}
|
|
1725
1821
|
|
|
@@ -2859,14 +2955,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2859
2955
|
return s2 !== null ? [s2] : [];
|
|
2860
2956
|
}
|
|
2861
2957
|
if (pv === RDF_NS + 'first') {
|
|
2862
|
-
return evalListFirstLikeBuiltin(g.s, g.o, subst);
|
|
2958
|
+
return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2863
2959
|
}
|
|
2864
2960
|
|
|
2865
2961
|
// list:rest and rdf:rest
|
|
2866
2962
|
// true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
|
|
2867
2963
|
// Schema: $s+ list:rest $o-
|
|
2868
2964
|
if (pv === LIST_NS + 'rest') {
|
|
2869
|
-
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2965
|
+
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2870
2966
|
const xs = __listElemsForBuiltin(g.s, facts);
|
|
2871
2967
|
if (!xs || !xs.length) return [];
|
|
2872
2968
|
const rest = new ListTerm(xs.slice(1));
|
|
@@ -2874,7 +2970,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2874
2970
|
return s2 !== null ? [s2] : [];
|
|
2875
2971
|
}
|
|
2876
2972
|
if (pv === RDF_NS + 'rest') {
|
|
2877
|
-
return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2973
|
+
return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2878
2974
|
}
|
|
2879
2975
|
|
|
2880
2976
|
// list:iterate
|
|
@@ -7918,7 +8014,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
7918
8014
|
const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
|
|
7919
8015
|
const shouldTreatAsBuiltin =
|
|
7920
8016
|
isBuiltinPred(goal0.p) &&
|
|
7921
|
-
!(
|
|
8017
|
+
!(
|
|
8018
|
+
isRdfFirstOrRest &&
|
|
8019
|
+
!(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
|
|
8020
|
+
);
|
|
7922
8021
|
|
|
7923
8022
|
if (shouldTreatAsBuiltin) {
|
|
7924
8023
|
const remaining = max - results.length;
|
|
@@ -11703,14 +11802,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
|
|
|
11703
11802
|
const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
|
|
11704
11803
|
const RDF_JSON_DT = RDF_NS + 'JSON';
|
|
11705
11804
|
|
|
11805
|
+
function parseUriReferenceForResolution(uri) {
|
|
11806
|
+
// RFC 3986 Appendix B-style component parser, with the scheme tightened to
|
|
11807
|
+
// the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
|
|
11808
|
+
// empty query is defined, while no `?` means undefined.
|
|
11809
|
+
const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
|
|
11810
|
+
if (!m) return null;
|
|
11811
|
+
return {
|
|
11812
|
+
scheme: m[2] !== undefined ? m[2] : undefined,
|
|
11813
|
+
authority: m[4] !== undefined ? m[4] : undefined,
|
|
11814
|
+
path: m[5] || '',
|
|
11815
|
+
query: m[6] !== undefined ? (m[7] || '') : undefined,
|
|
11816
|
+
fragment: m[8] !== undefined ? (m[9] || '') : undefined,
|
|
11817
|
+
};
|
|
11818
|
+
}
|
|
11819
|
+
|
|
11820
|
+
function recomposeUriReference(parts) {
|
|
11821
|
+
let out = '';
|
|
11822
|
+
if (parts.scheme !== undefined) out += `${parts.scheme}:`;
|
|
11823
|
+
if (parts.authority !== undefined) out += `//${parts.authority}`;
|
|
11824
|
+
out += parts.path || '';
|
|
11825
|
+
if (parts.query !== undefined) out += `?${parts.query}`;
|
|
11826
|
+
if (parts.fragment !== undefined) out += `#${parts.fragment}`;
|
|
11827
|
+
return out;
|
|
11828
|
+
}
|
|
11829
|
+
|
|
11830
|
+
function removeLastPathSegment(path) {
|
|
11831
|
+
if (!path) return '';
|
|
11832
|
+
const i = path.lastIndexOf('/');
|
|
11833
|
+
if (i < 0) return '';
|
|
11834
|
+
if (i === 0) return '';
|
|
11835
|
+
return path.slice(0, i);
|
|
11836
|
+
}
|
|
11837
|
+
|
|
11838
|
+
function removeDotSegments(path) {
|
|
11839
|
+
// RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
|
|
11840
|
+
// Eyeling preserves IRI spelling (for example, it does not add a trailing
|
|
11841
|
+
// slash to `http://example.org`) while still normalizing `.` and `..` path
|
|
11842
|
+
// segments as required by section 5.2.2.
|
|
11843
|
+
let input = String(path || '');
|
|
11844
|
+
let output = '';
|
|
11845
|
+
|
|
11846
|
+
while (input.length > 0) {
|
|
11847
|
+
if (input.startsWith('../')) {
|
|
11848
|
+
input = input.slice(3);
|
|
11849
|
+
} else if (input.startsWith('./')) {
|
|
11850
|
+
input = input.slice(2);
|
|
11851
|
+
} else if (input.startsWith('/./')) {
|
|
11852
|
+
input = `/${input.slice(3)}`;
|
|
11853
|
+
} else if (input === '/.') {
|
|
11854
|
+
input = '/';
|
|
11855
|
+
} else if (input.startsWith('/../')) {
|
|
11856
|
+
input = `/${input.slice(4)}`;
|
|
11857
|
+
output = removeLastPathSegment(output);
|
|
11858
|
+
} else if (input === '/..') {
|
|
11859
|
+
input = '/';
|
|
11860
|
+
output = removeLastPathSegment(output);
|
|
11861
|
+
} else if (input === '.' || input === '..') {
|
|
11862
|
+
input = '';
|
|
11863
|
+
} else {
|
|
11864
|
+
let segmentEnd;
|
|
11865
|
+
if (input[0] === '/') {
|
|
11866
|
+
segmentEnd = input.indexOf('/', 1);
|
|
11867
|
+
} else {
|
|
11868
|
+
segmentEnd = input.indexOf('/');
|
|
11869
|
+
}
|
|
11870
|
+
|
|
11871
|
+
if (segmentEnd < 0) {
|
|
11872
|
+
output += input;
|
|
11873
|
+
input = '';
|
|
11874
|
+
} else {
|
|
11875
|
+
output += input.slice(0, segmentEnd);
|
|
11876
|
+
input = input.slice(segmentEnd);
|
|
11877
|
+
}
|
|
11878
|
+
}
|
|
11879
|
+
}
|
|
11880
|
+
|
|
11881
|
+
return output;
|
|
11882
|
+
}
|
|
11883
|
+
|
|
11884
|
+
function mergePaths(base, refPath) {
|
|
11885
|
+
if (base.authority !== undefined && base.path === '') {
|
|
11886
|
+
return `/${refPath}`;
|
|
11887
|
+
}
|
|
11888
|
+
const i = base.path.lastIndexOf('/');
|
|
11889
|
+
if (i < 0) return refPath;
|
|
11890
|
+
return `${base.path.slice(0, i + 1)}${refPath}`;
|
|
11891
|
+
}
|
|
11892
|
+
|
|
11706
11893
|
function resolveIriRef(ref, base) {
|
|
11707
|
-
|
|
11708
|
-
if (
|
|
11709
|
-
|
|
11710
|
-
|
|
11711
|
-
|
|
11712
|
-
|
|
11894
|
+
const r = parseUriReferenceForResolution(ref);
|
|
11895
|
+
if (!r) return ref;
|
|
11896
|
+
|
|
11897
|
+
const baseParts = base ? parseUriReferenceForResolution(base) : null;
|
|
11898
|
+
|
|
11899
|
+
// Absolute references do not need a base, but RFC 3986 section 5.2.2 still
|
|
11900
|
+
// applies remove_dot_segments(R.path) when R.scheme is defined.
|
|
11901
|
+
if (r.scheme !== undefined) {
|
|
11902
|
+
return recomposeUriReference({
|
|
11903
|
+
scheme: r.scheme,
|
|
11904
|
+
authority: r.authority,
|
|
11905
|
+
path: removeDotSegments(r.path),
|
|
11906
|
+
query: r.query,
|
|
11907
|
+
fragment: r.fragment,
|
|
11908
|
+
});
|
|
11713
11909
|
}
|
|
11910
|
+
|
|
11911
|
+
// Without a usable base, preserve relative references as written.
|
|
11912
|
+
if (!baseParts || baseParts.scheme === undefined) return ref;
|
|
11913
|
+
|
|
11914
|
+
const t = {
|
|
11915
|
+
scheme: baseParts.scheme,
|
|
11916
|
+
authority: undefined,
|
|
11917
|
+
path: '',
|
|
11918
|
+
query: undefined,
|
|
11919
|
+
fragment: r.fragment,
|
|
11920
|
+
};
|
|
11921
|
+
|
|
11922
|
+
if (r.authority !== undefined) {
|
|
11923
|
+
t.authority = r.authority;
|
|
11924
|
+
t.path = removeDotSegments(r.path);
|
|
11925
|
+
t.query = r.query;
|
|
11926
|
+
} else if (r.path === '') {
|
|
11927
|
+
t.authority = baseParts.authority;
|
|
11928
|
+
t.path = baseParts.path;
|
|
11929
|
+
t.query = r.query !== undefined ? r.query : baseParts.query;
|
|
11930
|
+
} else {
|
|
11931
|
+
t.authority = baseParts.authority;
|
|
11932
|
+
if (r.path.startsWith('/')) {
|
|
11933
|
+
t.path = removeDotSegments(r.path);
|
|
11934
|
+
} else {
|
|
11935
|
+
t.path = removeDotSegments(mergePaths(baseParts, r.path));
|
|
11936
|
+
}
|
|
11937
|
+
t.query = r.query;
|
|
11938
|
+
}
|
|
11939
|
+
|
|
11940
|
+
return recomposeUriReference(t);
|
|
11714
11941
|
}
|
|
11715
11942
|
|
|
11716
11943
|
// -----------------------------------------------------------------------------
|
package/lib/builtins.js
CHANGED
|
@@ -1680,15 +1680,85 @@ function __listElemsForBuiltin(listLike, facts) {
|
|
|
1680
1680
|
return null;
|
|
1681
1681
|
}
|
|
1682
1682
|
|
|
1683
|
-
function
|
|
1684
|
-
if (
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1683
|
+
function __pushBuiltinDeltaLimited(out, delta, maxResults) {
|
|
1684
|
+
if (delta === null) return false;
|
|
1685
|
+
out.push(delta);
|
|
1686
|
+
return typeof maxResults === 'number' && maxResults > 0 && out.length >= maxResults;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function __collectListLikeTermsFromTerm(t, out, seen) {
|
|
1690
|
+
if (t instanceof ListTerm || t instanceof OpenListTerm) {
|
|
1691
|
+
const k = termFastKey ? termFastKey(t) : null;
|
|
1692
|
+
if (k === null || !seen.has(k)) {
|
|
1693
|
+
if (k !== null) seen.add(k);
|
|
1694
|
+
out.push(t);
|
|
1695
|
+
}
|
|
1696
|
+
if (t instanceof ListTerm) {
|
|
1697
|
+
for (const e of t.elems) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1698
|
+
} else {
|
|
1699
|
+
for (const e of t.prefix) __collectListLikeTermsFromTerm(e, out, seen);
|
|
1700
|
+
}
|
|
1701
|
+
return;
|
|
1702
|
+
}
|
|
1703
|
+
if (t instanceof GraphTerm) {
|
|
1704
|
+
for (const tr of t.triples) {
|
|
1705
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1706
|
+
__collectListLikeTermsFromTerm(tr.p, out, seen);
|
|
1707
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function __collectListLikeTermsFromFacts(facts) {
|
|
1713
|
+
const out = [];
|
|
1714
|
+
const seen = new Set();
|
|
1715
|
+
for (const tr of facts) {
|
|
1716
|
+
__collectListLikeTermsFromTerm(tr.s, out, seen);
|
|
1717
|
+
__collectListLikeTermsFromTerm(tr.o, out, seen);
|
|
1718
|
+
}
|
|
1719
|
+
return out;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
function evalListFirstLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1723
|
+
if (sTerm instanceof ListTerm) {
|
|
1724
|
+
if (!sTerm.elems.length) return [];
|
|
1725
|
+
const first = sTerm.elems[0];
|
|
1726
|
+
const s2 = unifyTerm(oTerm, first, subst);
|
|
1727
|
+
return s2 !== null ? [s2] : [];
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// For a variable subject, enumerate already-existing collection terms.
|
|
1731
|
+
// This lets a rule body such as `_:x rdf:first 1; rdf:rest rdf:nil`
|
|
1732
|
+
// bind `_:x` to an N3 collection literal `(1)` used elsewhere as a term.
|
|
1733
|
+
// Also include ordinary rdf:first facts so the builtin path does not hide
|
|
1734
|
+
// RDF-serialized lists when the subject starts unbound.
|
|
1735
|
+
if (sTerm instanceof Var) {
|
|
1736
|
+
const out = [];
|
|
1737
|
+
|
|
1738
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1739
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1740
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1741
|
+
if (s2 === null) continue;
|
|
1742
|
+
s2 = unifyTerm(oTerm, listTerm.elems[0], s2);
|
|
1743
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
const RDF_FIRST = RDF_NS + 'first';
|
|
1747
|
+
for (const tr of facts) {
|
|
1748
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_FIRST) continue;
|
|
1749
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1750
|
+
if (s2 === null) continue;
|
|
1751
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1752
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
return out;
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
return [];
|
|
1689
1759
|
}
|
|
1690
1760
|
|
|
1691
|
-
function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
1761
|
+
function evalListRestLikeBuiltin(sTerm, oTerm, subst, facts, maxResults) {
|
|
1692
1762
|
// Closed list: (a b c) -> (b c)
|
|
1693
1763
|
if (sTerm instanceof ListTerm) {
|
|
1694
1764
|
if (!sTerm.elems.length) return [];
|
|
@@ -1709,6 +1779,32 @@ function evalListRestLikeBuiltin(sTerm, oTerm, subst) {
|
|
|
1709
1779
|
return s2 !== null ? [s2] : [];
|
|
1710
1780
|
}
|
|
1711
1781
|
|
|
1782
|
+
// See evalListFirstLikeBuiltin(): if the collection subject is still a
|
|
1783
|
+
// variable, enumerate known list literals and ordinary rdf:rest facts.
|
|
1784
|
+
if (sTerm instanceof Var) {
|
|
1785
|
+
const out = [];
|
|
1786
|
+
|
|
1787
|
+
for (const listTerm of __collectListLikeTermsFromFacts(facts)) {
|
|
1788
|
+
if (!(listTerm instanceof ListTerm) || !listTerm.elems.length) continue;
|
|
1789
|
+
let s2 = unifyTerm(sTerm, listTerm, subst);
|
|
1790
|
+
if (s2 === null) continue;
|
|
1791
|
+
const rest = new ListTerm(listTerm.elems.slice(1));
|
|
1792
|
+
s2 = unifyTerm(oTerm, rest, s2);
|
|
1793
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
const RDF_REST = RDF_NS + 'rest';
|
|
1797
|
+
for (const tr of facts) {
|
|
1798
|
+
if (!(tr.p instanceof Iri) || tr.p.value !== RDF_REST) continue;
|
|
1799
|
+
let s2 = unifyTerm(sTerm, tr.s, subst);
|
|
1800
|
+
if (s2 === null) continue;
|
|
1801
|
+
s2 = unifyTerm(oTerm, tr.o, s2);
|
|
1802
|
+
if (__pushBuiltinDeltaLimited(out, s2, maxResults)) return out;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
return out;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1712
1808
|
return [];
|
|
1713
1809
|
}
|
|
1714
1810
|
|
|
@@ -2848,14 +2944,14 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2848
2944
|
return s2 !== null ? [s2] : [];
|
|
2849
2945
|
}
|
|
2850
2946
|
if (pv === RDF_NS + 'first') {
|
|
2851
|
-
return evalListFirstLikeBuiltin(g.s, g.o, subst);
|
|
2947
|
+
return evalListFirstLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2852
2948
|
}
|
|
2853
2949
|
|
|
2854
2950
|
// list:rest and rdf:rest
|
|
2855
2951
|
// true iff $s is a (non-empty) list and $o is the rest (tail) of that list.
|
|
2856
2952
|
// Schema: $s+ list:rest $o-
|
|
2857
2953
|
if (pv === LIST_NS + 'rest') {
|
|
2858
|
-
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2954
|
+
if (g.s instanceof ListTerm || g.s instanceof OpenListTerm) return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2859
2955
|
const xs = __listElemsForBuiltin(g.s, facts);
|
|
2860
2956
|
if (!xs || !xs.length) return [];
|
|
2861
2957
|
const rest = new ListTerm(xs.slice(1));
|
|
@@ -2863,7 +2959,7 @@ function evalBuiltin(goal, subst, facts, backRules, depth, varGen, maxResults) {
|
|
|
2863
2959
|
return s2 !== null ? [s2] : [];
|
|
2864
2960
|
}
|
|
2865
2961
|
if (pv === RDF_NS + 'rest') {
|
|
2866
|
-
return evalListRestLikeBuiltin(g.s, g.o, subst);
|
|
2962
|
+
return evalListRestLikeBuiltin(g.s, g.o, subst, facts, maxResults);
|
|
2867
2963
|
}
|
|
2868
2964
|
|
|
2869
2965
|
// list:iterate
|
package/lib/engine.js
CHANGED
|
@@ -2600,7 +2600,10 @@ function proveGoals(goals, subst, facts, backRules, depth, visited, varGen, maxR
|
|
|
2600
2600
|
const isRdfFirstOrRest = goalPredicateIri === RDF_NS + 'first' || goalPredicateIri === RDF_NS + 'rest';
|
|
2601
2601
|
const shouldTreatAsBuiltin =
|
|
2602
2602
|
isBuiltinPred(goal0.p) &&
|
|
2603
|
-
!(
|
|
2603
|
+
!(
|
|
2604
|
+
isRdfFirstOrRest &&
|
|
2605
|
+
!(goal0.s instanceof ListTerm || goal0.s instanceof OpenListTerm || goal0.s instanceof Var)
|
|
2606
|
+
);
|
|
2604
2607
|
|
|
2605
2608
|
if (shouldTreatAsBuiltin) {
|
|
2606
2609
|
const remaining = max - results.length;
|
package/lib/prelude.js
CHANGED
|
@@ -24,14 +24,142 @@ const STRING_NS = 'http://www.w3.org/2000/10/swap/string#';
|
|
|
24
24
|
const SKOLEM_NS = 'https://eyereasoner.github.io/.well-known/genid/';
|
|
25
25
|
const RDF_JSON_DT = RDF_NS + 'JSON';
|
|
26
26
|
|
|
27
|
+
function parseUriReferenceForResolution(uri) {
|
|
28
|
+
// RFC 3986 Appendix B-style component parser, with the scheme tightened to
|
|
29
|
+
// the RFC scheme grammar. Capturing delimiter presence matters: `?` with an
|
|
30
|
+
// empty query is defined, while no `?` means undefined.
|
|
31
|
+
const m = /^(([A-Za-z][A-Za-z0-9+.-]*):)?(\/\/([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/u.exec(String(uri));
|
|
32
|
+
if (!m) return null;
|
|
33
|
+
return {
|
|
34
|
+
scheme: m[2] !== undefined ? m[2] : undefined,
|
|
35
|
+
authority: m[4] !== undefined ? m[4] : undefined,
|
|
36
|
+
path: m[5] || '',
|
|
37
|
+
query: m[6] !== undefined ? (m[7] || '') : undefined,
|
|
38
|
+
fragment: m[8] !== undefined ? (m[9] || '') : undefined,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function recomposeUriReference(parts) {
|
|
43
|
+
let out = '';
|
|
44
|
+
if (parts.scheme !== undefined) out += `${parts.scheme}:`;
|
|
45
|
+
if (parts.authority !== undefined) out += `//${parts.authority}`;
|
|
46
|
+
out += parts.path || '';
|
|
47
|
+
if (parts.query !== undefined) out += `?${parts.query}`;
|
|
48
|
+
if (parts.fragment !== undefined) out += `#${parts.fragment}`;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeLastPathSegment(path) {
|
|
53
|
+
if (!path) return '';
|
|
54
|
+
const i = path.lastIndexOf('/');
|
|
55
|
+
if (i < 0) return '';
|
|
56
|
+
if (i === 0) return '';
|
|
57
|
+
return path.slice(0, i);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function removeDotSegments(path) {
|
|
61
|
+
// RFC 3986 section 5.2.4. This deliberately avoids WHATWG URL parsing so
|
|
62
|
+
// Eyeling preserves IRI spelling (for example, it does not add a trailing
|
|
63
|
+
// slash to `http://example.org`) while still normalizing `.` and `..` path
|
|
64
|
+
// segments as required by section 5.2.2.
|
|
65
|
+
let input = String(path || '');
|
|
66
|
+
let output = '';
|
|
67
|
+
|
|
68
|
+
while (input.length > 0) {
|
|
69
|
+
if (input.startsWith('../')) {
|
|
70
|
+
input = input.slice(3);
|
|
71
|
+
} else if (input.startsWith('./')) {
|
|
72
|
+
input = input.slice(2);
|
|
73
|
+
} else if (input.startsWith('/./')) {
|
|
74
|
+
input = `/${input.slice(3)}`;
|
|
75
|
+
} else if (input === '/.') {
|
|
76
|
+
input = '/';
|
|
77
|
+
} else if (input.startsWith('/../')) {
|
|
78
|
+
input = `/${input.slice(4)}`;
|
|
79
|
+
output = removeLastPathSegment(output);
|
|
80
|
+
} else if (input === '/..') {
|
|
81
|
+
input = '/';
|
|
82
|
+
output = removeLastPathSegment(output);
|
|
83
|
+
} else if (input === '.' || input === '..') {
|
|
84
|
+
input = '';
|
|
85
|
+
} else {
|
|
86
|
+
let segmentEnd;
|
|
87
|
+
if (input[0] === '/') {
|
|
88
|
+
segmentEnd = input.indexOf('/', 1);
|
|
89
|
+
} else {
|
|
90
|
+
segmentEnd = input.indexOf('/');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (segmentEnd < 0) {
|
|
94
|
+
output += input;
|
|
95
|
+
input = '';
|
|
96
|
+
} else {
|
|
97
|
+
output += input.slice(0, segmentEnd);
|
|
98
|
+
input = input.slice(segmentEnd);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mergePaths(base, refPath) {
|
|
107
|
+
if (base.authority !== undefined && base.path === '') {
|
|
108
|
+
return `/${refPath}`;
|
|
109
|
+
}
|
|
110
|
+
const i = base.path.lastIndexOf('/');
|
|
111
|
+
if (i < 0) return refPath;
|
|
112
|
+
return `${base.path.slice(0, i + 1)}${refPath}`;
|
|
113
|
+
}
|
|
114
|
+
|
|
27
115
|
function resolveIriRef(ref, base) {
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
116
|
+
const r = parseUriReferenceForResolution(ref);
|
|
117
|
+
if (!r) return ref;
|
|
118
|
+
|
|
119
|
+
const baseParts = base ? parseUriReferenceForResolution(base) : null;
|
|
120
|
+
|
|
121
|
+
// Absolute references do not need a base, but RFC 3986 section 5.2.2 still
|
|
122
|
+
// applies remove_dot_segments(R.path) when R.scheme is defined.
|
|
123
|
+
if (r.scheme !== undefined) {
|
|
124
|
+
return recomposeUriReference({
|
|
125
|
+
scheme: r.scheme,
|
|
126
|
+
authority: r.authority,
|
|
127
|
+
path: removeDotSegments(r.path),
|
|
128
|
+
query: r.query,
|
|
129
|
+
fragment: r.fragment,
|
|
130
|
+
});
|
|
34
131
|
}
|
|
132
|
+
|
|
133
|
+
// Without a usable base, preserve relative references as written.
|
|
134
|
+
if (!baseParts || baseParts.scheme === undefined) return ref;
|
|
135
|
+
|
|
136
|
+
const t = {
|
|
137
|
+
scheme: baseParts.scheme,
|
|
138
|
+
authority: undefined,
|
|
139
|
+
path: '',
|
|
140
|
+
query: undefined,
|
|
141
|
+
fragment: r.fragment,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
if (r.authority !== undefined) {
|
|
145
|
+
t.authority = r.authority;
|
|
146
|
+
t.path = removeDotSegments(r.path);
|
|
147
|
+
t.query = r.query;
|
|
148
|
+
} else if (r.path === '') {
|
|
149
|
+
t.authority = baseParts.authority;
|
|
150
|
+
t.path = baseParts.path;
|
|
151
|
+
t.query = r.query !== undefined ? r.query : baseParts.query;
|
|
152
|
+
} else {
|
|
153
|
+
t.authority = baseParts.authority;
|
|
154
|
+
if (r.path.startsWith('/')) {
|
|
155
|
+
t.path = removeDotSegments(r.path);
|
|
156
|
+
} else {
|
|
157
|
+
t.path = removeDotSegments(mergePaths(baseParts, r.path));
|
|
158
|
+
}
|
|
159
|
+
t.query = r.query;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return recomposeUriReference(t);
|
|
35
163
|
}
|
|
36
164
|
|
|
37
165
|
// -----------------------------------------------------------------------------
|
package/package.json
CHANGED
package/test/api.test.js
CHANGED
|
@@ -1245,6 +1245,35 @@ ${U('o1')} ${U('path')} (${U('c')} ${U('d')}).
|
|
|
1245
1245
|
],
|
|
1246
1246
|
},
|
|
1247
1247
|
|
|
1248
|
+
{
|
|
1249
|
+
name: '49c rdf:first/rest bind an unbound rule blank to an N3 list literal',
|
|
1250
|
+
opt: { proofComments: false },
|
|
1251
|
+
input: `@prefix : <http://example.org/> .
|
|
1252
|
+
|
|
1253
|
+
(1) <http://a.example/p> <http://a.example/o> .
|
|
1254
|
+
|
|
1255
|
+
{
|
|
1256
|
+
_:el1 rdf:first 1 .
|
|
1257
|
+
_:el1 rdf:rest rdf:nil .
|
|
1258
|
+
_:el1 <http://a.example/p> <http://a.example/o> .
|
|
1259
|
+
}
|
|
1260
|
+
=>
|
|
1261
|
+
{
|
|
1262
|
+
:result :has :success-literal-25 .
|
|
1263
|
+
}.
|
|
1264
|
+
|
|
1265
|
+
{ :result :has :success-literal-25 . }
|
|
1266
|
+
=>
|
|
1267
|
+
{
|
|
1268
|
+
:test :is true .
|
|
1269
|
+
}.
|
|
1270
|
+
`,
|
|
1271
|
+
expect: [
|
|
1272
|
+
/:result\s+:has\s+:success-literal-25\s*\./,
|
|
1273
|
+
/:test\s+:is\s+true\s*\./,
|
|
1274
|
+
],
|
|
1275
|
+
},
|
|
1276
|
+
|
|
1248
1277
|
{
|
|
1249
1278
|
name: '50 rdf collection materialization: rdf:first/rdf:rest triples become list terms',
|
|
1250
1279
|
opt: { proofComments: false },
|
package/test/playground.test.js
CHANGED
|
@@ -873,6 +873,11 @@ ${JSON.stringify(last, null, 2)}`);
|
|
|
873
873
|
const outputStringProgram = `@prefix : <#> .
|
|
874
874
|
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
875
875
|
:report log:outputString "## Hello from output string\n\nLine 2 with **bold** and [Eyeling](https://example.org/eyeling)\n" .
|
|
876
|
+
`;
|
|
877
|
+
const baseOnlyMarkdownProgram = `@base <https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/smoke-arithmetic.n3> .
|
|
878
|
+
@prefix : <#> .
|
|
879
|
+
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
880
|
+
:report log:outputString "# stateurl link base\n\n[N3 rules](../smoke-arithmetic.n3)\n[Input TriG](../input/smoke-arithmetic.trig)\n" .
|
|
876
881
|
`;
|
|
877
882
|
const logQueryTurtleProgram = `@prefix : <#> .
|
|
878
883
|
@prefix log: <http://www.w3.org/2000/10/swap/log#> .
|
|
@@ -972,17 +977,45 @@ ${JSON.stringify(last, null, 2)}`);
|
|
|
972
977
|
assert.equal(renderedAgain.renderedTabSelected, true, 'Expected Rendered tab to be selectable again');
|
|
973
978
|
endTest();
|
|
974
979
|
|
|
975
|
-
// 5)
|
|
980
|
+
// 5) Shared state files may only restore editor text. If that text came from a repository
|
|
981
|
+
// example, the injected @base line should still give Markdown links the static output-page base.
|
|
982
|
+
beginTest('playground resolves Markdown links from restored example base directives');
|
|
983
|
+
await setProgram(baseOnlyMarkdownProgram);
|
|
984
|
+
await clickRun();
|
|
985
|
+
const baseOnlyMarkdown = await waitForState(
|
|
986
|
+
'base-only Markdown output completion',
|
|
987
|
+
(st) =>
|
|
988
|
+
String(st.status || '')
|
|
989
|
+
.trim()
|
|
990
|
+
.startsWith('Done') && /stateurl link base/i.test(String(st.output || '')),
|
|
991
|
+
20000,
|
|
992
|
+
);
|
|
993
|
+
assert.match(
|
|
994
|
+
baseOnlyMarkdown.renderedHtml,
|
|
995
|
+
new RegExp('href="' + started.baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/examples/smoke-arithmetic\\.n3"'),
|
|
996
|
+
'Expected restored-state Markdown source links to resolve against the static output page',
|
|
997
|
+
);
|
|
998
|
+
assert.match(
|
|
999
|
+
baseOnlyMarkdown.renderedHtml,
|
|
1000
|
+
new RegExp('href="' + started.baseUrl.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '/examples/input/smoke-arithmetic\\.trig"'),
|
|
1001
|
+
'Expected restored-state Markdown TriG links to resolve against the static output page',
|
|
1002
|
+
);
|
|
1003
|
+
endTest();
|
|
1004
|
+
|
|
1005
|
+
// 6) Normal editing should not keep rewriting the browser URL with raw N3 content.
|
|
976
1006
|
beginTest('playground keeps the live URL short and creates compact share links on demand');
|
|
977
|
-
|
|
1007
|
+
await setProgram(outputStringProgram);
|
|
1008
|
+
const compactShareState = await getPlaygroundState();
|
|
1009
|
+
assert.doesNotMatch(compactShareState.href, /[?&](?:edit|program)=/, 'Expected live URL to avoid raw editor content');
|
|
978
1010
|
const compactShareUrl = await makeShareUrlInPage();
|
|
1011
|
+
const rawEditorUrlLength = playgroundUrl.length + '?edit='.length + encodeURIComponent(outputStringProgram).length;
|
|
979
1012
|
assert.match(compactShareUrl, /[?&]state=/, 'Expected an on-demand compact state parameter');
|
|
980
1013
|
assert.doesNotMatch(compactShareUrl, /[?&](?:edit|program)=/, 'Expected share link to avoid raw edit/program params');
|
|
981
|
-
assert.ok(compactShareUrl.length <
|
|
982
|
-
assert.equal(
|
|
1014
|
+
assert.ok(compactShareUrl.length < rawEditorUrlLength, 'Expected compact share URL to be shorter than raw editor URL');
|
|
1015
|
+
assert.equal(compactShareState.gistShareHidden, true, 'Expected ordinary compact share links to keep the Gist share option hidden');
|
|
983
1016
|
endTest();
|
|
984
1017
|
|
|
985
|
-
//
|
|
1018
|
+
// 7) Very large edited programs should offer a Gist-backed share option instead of only a huge link.
|
|
986
1019
|
beginTest('playground offers a Gist-backed option for oversized state links');
|
|
987
1020
|
const longShareProgram = Array.from({ length: 1400 }, (_, i) => {
|
|
988
1021
|
const n = String(i).padStart(4, '0');
|
|
@@ -1015,7 +1048,7 @@ ${JSON.stringify(last, null, 2)}`);
|
|
|
1015
1048
|
assert.match(String(gistShare.seen.options.body || ''), /\\"e\\":/, 'Expected compact editor state in the Gist payload');
|
|
1016
1049
|
endTest();
|
|
1017
1050
|
|
|
1018
|
-
//
|
|
1051
|
+
// 8) log:query can produce Turtle; that should stay in plain source output without Markdown tabs.
|
|
1019
1052
|
beginTest('playground hides markdown tabs for Turtle log:query output');
|
|
1020
1053
|
await setProgram(logQueryTurtleProgram);
|
|
1021
1054
|
await clickRun();
|
|
@@ -1035,7 +1068,7 @@ ${JSON.stringify(last, null, 2)}`);
|
|
|
1035
1068
|
assert.equal(logQueryTurtle.sourceHidden, false, 'Expected Turtle log:query output to show source directly');
|
|
1036
1069
|
endTest();
|
|
1037
1070
|
|
|
1038
|
-
//
|
|
1071
|
+
// 9) URL-loaded examples should auto-load matching examples/input/<stem>.trig and run in RDF/TriG mode.
|
|
1039
1072
|
beginTest('playground auto-loads companion TriG sidecars and uses RDF/TriG mode');
|
|
1040
1073
|
await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/smoke-arithmetic.n3');
|
|
1041
1074
|
const smokeLoaded = await waitForState(
|
|
@@ -1082,7 +1115,7 @@ ${JSON.stringify(last, null, 2)}`);
|
|
|
1082
1115
|
assert.equal(smokeRenderedAgain.sourceHidden, true, 'Expected smoke-arithmetic source editor to hide again');
|
|
1083
1116
|
endTest();
|
|
1084
1117
|
|
|
1085
|
-
//
|
|
1118
|
+
// 10) URL-loaded repository examples should auto-load matching examples/builtin/<stem>.js.
|
|
1086
1119
|
beginTest('playground auto-loads a companion example builtin for URL-loaded Sudoku');
|
|
1087
1120
|
await loadUrlIntoEditor('https://raw.githubusercontent.com/eyereasoner/eyeling/refs/heads/main/examples/sudoku.n3');
|
|
1088
1121
|
await waitForState(
|