@kgdiff/formats 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +68 -2
- package/dist/detect.d.ts +47 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/detect.js +425 -0
- package/dist/detect.js.map +1 -0
- package/dist/index.d.ts +203 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +114 -0
- package/dist/index.js.map +1 -0
- package/dist/iri.d.ts +13 -0
- package/dist/iri.d.ts.map +1 -0
- package/dist/iri.js +91 -0
- package/dist/iri.js.map +1 -0
- package/dist/json.d.ts +31 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +787 -0
- package/dist/json.js.map +1 -0
- package/dist/jsonld.d.ts +16 -0
- package/dist/jsonld.d.ts.map +1 -0
- package/dist/jsonld.js +1249 -0
- package/dist/jsonld.js.map +1 -0
- package/dist/obo.d.ts +25 -0
- package/dist/obo.d.ts.map +1 -0
- package/dist/obo.js +478 -0
- package/dist/obo.js.map +1 -0
- package/dist/prefixes.d.ts +49 -0
- package/dist/prefixes.d.ts.map +1 -0
- package/dist/prefixes.js +177 -0
- package/dist/prefixes.js.map +1 -0
- package/dist/rdf.d.ts +46 -0
- package/dist/rdf.d.ts.map +1 -0
- package/dist/rdf.js +1047 -0
- package/dist/rdf.js.map +1 -0
- package/dist/rdfxml.d.ts +8 -0
- package/dist/rdfxml.d.ts.map +1 -0
- package/dist/rdfxml.js +529 -0
- package/dist/rdfxml.js.map +1 -0
- package/dist/shared.d.ts +117 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +355 -0
- package/dist/shared.js.map +1 -0
- package/dist/xml.d.ts +50 -0
- package/dist/xml.d.ts.map +1 -0
- package/dist/xml.js +508 -0
- package/dist/xml.js.map +1 -0
- package/package.json +55 -3
- package/src/detect.ts +435 -0
- package/src/index.ts +296 -0
- package/src/iri.ts +86 -0
- package/src/json.ts +889 -0
- package/src/jsonld.ts +1376 -0
- package/src/obo.ts +557 -0
- package/src/prefixes.ts +195 -0
- package/src/rdf.ts +1181 -0
- package/src/rdfxml.ts +633 -0
- package/src/shared.ts +408 -0
- package/src/xml.ts +550 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @kgdiff/formats: turn real-world graph files into kgdiff `GraphInput`s.
|
|
3
|
+
*
|
|
4
|
+
* Isomorphic (browser + Node), synchronous, no file-system or network access: callers pass
|
|
5
|
+
* text. Every parser maps its source into the same `GraphInput` conventions
|
|
6
|
+
* (docs/architecture.md §Formats) so diffs across formats behave identically.
|
|
7
|
+
*
|
|
8
|
+
* Output is deterministic: the same text and options always give an identical result, with
|
|
9
|
+
* nodes sorted by id, edges by (source, predicate, target), attribute keys sorted and
|
|
10
|
+
* multi-valued fields as sorted arrays.
|
|
11
|
+
*/
|
|
12
|
+
import type { GraphInput } from '@kgdiff/core';
|
|
13
|
+
import { detect, FORMAT_EXTENSIONS, isJsonLd } from './detect.js';
|
|
14
|
+
import { parseJson, parseJsonText } from './json.js';
|
|
15
|
+
import { readJsonLd } from './jsonld.js';
|
|
16
|
+
import { parseObo } from './obo.js';
|
|
17
|
+
import { tableFor, WELL_KNOWN_PREFIXES } from './prefixes.js';
|
|
18
|
+
import { mapRdf, readN3 } from './rdf.js';
|
|
19
|
+
import { readRdfXml } from './rdfxml.js';
|
|
20
|
+
import { assertNever, invalidInput, isRecord, makeMeta, Warnings } from './shared.js';
|
|
21
|
+
|
|
22
|
+
export { FORMAT_EXTENSIONS };
|
|
23
|
+
|
|
24
|
+
export type FormatId =
|
|
25
|
+
/** kgdiff native JSON and the common JSON graph shapes (auto-detected). */
|
|
26
|
+
| 'json'
|
|
27
|
+
/** JSON-LD 1.1, with inline contexts or contexts from `JsonLdOptions.documentLoader`. */
|
|
28
|
+
| 'jsonld'
|
|
29
|
+
| 'turtle'
|
|
30
|
+
| 'ntriples'
|
|
31
|
+
| 'nquads'
|
|
32
|
+
| 'trig'
|
|
33
|
+
/** RDF/XML, including OWL ontologies saved by Protégé, the OWL API or ROBOT (`.owl`). */
|
|
34
|
+
| 'rdfxml'
|
|
35
|
+
| 'obo';
|
|
36
|
+
|
|
37
|
+
/** JSON graph shapes recognised by the `json` format (reported as `ParseResult.dialect`). */
|
|
38
|
+
export type JsonDialect = 'kgdiff' | 'd3' | 'cytoscape' | 'g6' | 'graphology' | 'vis';
|
|
39
|
+
|
|
40
|
+
/** Stable codes of recoverable parse warnings. */
|
|
41
|
+
export type ParseWarningCode =
|
|
42
|
+
/** RDF: quads from named graphs were merged into one graph. */
|
|
43
|
+
| 'named-graphs'
|
|
44
|
+
/** RDF: triples with RDF 1.2 triple terms outside reification were skipped. */
|
|
45
|
+
| 'triple-terms'
|
|
46
|
+
/** RDF: lists of resources became unordered edges. */
|
|
47
|
+
| 'rdf-list-resources'
|
|
48
|
+
/** RDF: several owl:Ontology headers; one was used for `meta`. */
|
|
49
|
+
| 'multiple-ontologies'
|
|
50
|
+
/** JSON (d3): nodes without ids got their array index as id. */
|
|
51
|
+
| 'json-index-ids'
|
|
52
|
+
/** JSON: element fields that are not part of the graph (rendering state, shape types…) were dropped. */
|
|
53
|
+
| 'json-ignored-fields'
|
|
54
|
+
/** JSON-LD: keys that expand to no IRI (no `@context` term, no `@vocab`) were dropped. */
|
|
55
|
+
| 'jsonld-unmapped-terms'
|
|
56
|
+
/** OBO: a stanza type other than Term / Typedef / Instance was skipped. */
|
|
57
|
+
| 'obo-unknown-stanza'
|
|
58
|
+
/** OBO: stanzas with the same id were merged. */
|
|
59
|
+
| 'obo-duplicate-id'
|
|
60
|
+
/** OBO: a term has several names; extra ones became aliases. */
|
|
61
|
+
| 'obo-multiple-names'
|
|
62
|
+
/** OBO: a `relationship` without relation or target was skipped. */
|
|
63
|
+
| 'obo-invalid-relationship'
|
|
64
|
+
/** OBO: a tag without a value was skipped. */
|
|
65
|
+
| 'obo-missing-value'
|
|
66
|
+
/** OBO: a `def` without quotes was kept verbatim. */
|
|
67
|
+
| 'obo-unquoted-def'
|
|
68
|
+
/** OBO: an `idspace` header without prefix or IRI was skipped. */
|
|
69
|
+
| 'obo-invalid-idspace';
|
|
70
|
+
|
|
71
|
+
export interface RdfOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Predicates used for node labels, in priority order (full IRIs or CURIEs with a known
|
|
74
|
+
* prefix). Default rdfs:label, skos:prefLabel, schema:name, dcterms:title, foaf:name.
|
|
75
|
+
* Language preference outranks predicate priority; unchosen label values become aliases.
|
|
76
|
+
*/
|
|
77
|
+
readonly labelPredicates?: readonly string[];
|
|
78
|
+
/** Predicates used for aliases. Default skos:altLabel and the oboInOwl synonym predicates. */
|
|
79
|
+
readonly aliasPredicates?: readonly string[];
|
|
80
|
+
/** Keep `rdf:type` as edges too (default false: types only become `node.type`). */
|
|
81
|
+
readonly typeEdges?: boolean;
|
|
82
|
+
/** Turn `C rdfs:subClassOf [ owl:onProperty R ; owl:someValuesFrom D ]` into an edge C –R→ D. Default true. */
|
|
83
|
+
readonly owlRestrictions?: boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Language preference for labels, e.g. ['zh', 'en']. Default: untagged, then 'en', then any.
|
|
86
|
+
* A tag also matches its subtags (`en` matches `en-GB`, exact tags first); untagged labels
|
|
87
|
+
* come after the listed languages unless `''` is listed explicitly. Ties break
|
|
88
|
+
* lexicographically.
|
|
89
|
+
*/
|
|
90
|
+
readonly languages?: readonly string[];
|
|
91
|
+
/**
|
|
92
|
+
* Extra prefixes used to compact IRIs (they win over declared prefixes). Unlike prefixes
|
|
93
|
+
* declared in the file, they also apply to identities, so pass the same map for every
|
|
94
|
+
* version you diff.
|
|
95
|
+
*/
|
|
96
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
97
|
+
/**
|
|
98
|
+
* Which prefixes compact identities: edge predicates, attribute keys, types and fallback
|
|
99
|
+
* labels. Default `well-known`: only the well-known prefixes, the OBO PURL rule and
|
|
100
|
+
* `prefixes`, so the same graph gets the same identities whether a file declares
|
|
101
|
+
* `@prefix ex:` or spells IRIs out (Turtle vs N-Triples). `declared` also uses the file's
|
|
102
|
+
* own `@prefix` declarations (prettier, but a renamed prefix then reads as a change).
|
|
103
|
+
* Declared prefixes are always returned in `ParseResult.prefixes` for display.
|
|
104
|
+
*/
|
|
105
|
+
readonly compactWith?: 'well-known' | 'declared';
|
|
106
|
+
/**
|
|
107
|
+
* Fold statement annotations (`owl:Axiom`, `rdf:Statement`, RDF 1.2 `{| … |}` reifiers)
|
|
108
|
+
* into the attrs of the annotated edge, or of the node as `<property>/<annotation>` for
|
|
109
|
+
* literal-valued statements. Default true; false keeps every annotation as a blank node.
|
|
110
|
+
*/
|
|
111
|
+
readonly foldAnnotations?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface JsonOptions {
|
|
115
|
+
/**
|
|
116
|
+
* Override field detection for custom shapes. Values are keys or dotted paths, looked up in
|
|
117
|
+
* the element's property record (`data` for Cytoscape / G6, `attributes` for graphology,
|
|
118
|
+
* the element itself otherwise), then in the raw element.
|
|
119
|
+
*/
|
|
120
|
+
readonly fields?: {
|
|
121
|
+
readonly id?: string;
|
|
122
|
+
readonly label?: string;
|
|
123
|
+
readonly type?: string;
|
|
124
|
+
readonly source?: string;
|
|
125
|
+
readonly target?: string;
|
|
126
|
+
readonly predicate?: string;
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface JsonLdOptions {
|
|
131
|
+
/**
|
|
132
|
+
* Provides remote JSON-LD contexts (`"@context": "https://…"`, `@import`): called once per
|
|
133
|
+
* URL, it returns the context document — parsed JSON or JSON text — or `undefined` when it
|
|
134
|
+
* has none. Must be synchronous; load what a document needs beforehand. Default: none, as
|
|
135
|
+
* kgdiff never fetches anything (for safety and reproducible diffs), so a remote context
|
|
136
|
+
* without a loader is an `invalid-input` error. Inline contexts need no loader.
|
|
137
|
+
*/
|
|
138
|
+
readonly documentLoader?: (url: string) => unknown;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface ParseOptions {
|
|
142
|
+
/** Default `auto`: detect from `filename` and content. */
|
|
143
|
+
readonly format?: FormatId | 'auto';
|
|
144
|
+
/** Used for format detection and as `meta.source`. */
|
|
145
|
+
readonly filename?: string;
|
|
146
|
+
/**
|
|
147
|
+
* Base IRI for relative IRIs in RDF (Turtle, RDF/XML, JSON-LD). Without one, relative IRIs
|
|
148
|
+
* stay relative.
|
|
149
|
+
*/
|
|
150
|
+
readonly baseIRI?: string;
|
|
151
|
+
/** Mapping options for every RDF syntax (Turtle family, RDF/XML, JSON-LD). */
|
|
152
|
+
readonly rdf?: RdfOptions;
|
|
153
|
+
readonly json?: JsonOptions;
|
|
154
|
+
readonly jsonld?: JsonLdOptions;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface ParseWarning {
|
|
158
|
+
readonly code: ParseWarningCode;
|
|
159
|
+
readonly message: string;
|
|
160
|
+
/** 1-based line, when known. */
|
|
161
|
+
readonly line?: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface ParseResult {
|
|
165
|
+
readonly graph: GraphInput;
|
|
166
|
+
/**
|
|
167
|
+
* The format that was read: the requested or detected one (a JSON-LD document read as
|
|
168
|
+
* `json` reports `jsonld`).
|
|
169
|
+
*/
|
|
170
|
+
readonly format: FormatId;
|
|
171
|
+
/** Detected JSON dialect (json format only). */
|
|
172
|
+
readonly dialect?: JsonDialect;
|
|
173
|
+
/** Prefix map (declared + well-known) for compact display of IRIs. */
|
|
174
|
+
readonly prefixes: Readonly<Record<string, string>>;
|
|
175
|
+
/**
|
|
176
|
+
* Display names the source gives its relations, by edge predicate: an RDF property's own
|
|
177
|
+
* label (`BFO:0000050` → “part of”), an OBO `[Typedef]` name, or kgdiff JSON
|
|
178
|
+
* `meta.predicateLabels`. Edge `label`s derive from the predicate alone, so declarations
|
|
179
|
+
* coming and going never relabel edges. Also stored as `graph.meta.predicateLabels`.
|
|
180
|
+
*/
|
|
181
|
+
readonly predicateLabels: Readonly<Record<string, string>>;
|
|
182
|
+
readonly warnings: readonly ParseWarning[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const FORMAT_IDS: ReadonlySet<string> = new Set(Object.keys(FORMAT_EXTENSIONS));
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Parse graph text in any supported format. Throws `KgdiffError('invalid-input')` with line
|
|
189
|
+
* info on syntax errors.
|
|
190
|
+
*
|
|
191
|
+
* - `format` defaults to `auto` (`detectFormat(text, filename)`).
|
|
192
|
+
* - `meta.source` is set to `filename` when given.
|
|
193
|
+
* - Every RDF syntax (Turtle, N-Triples, N-Quads, TriG, RDF/XML, JSON-LD) maps through the same
|
|
194
|
+
* conventions, so one graph gives the same `GraphInput` in any of them.
|
|
195
|
+
* - Synchronous and offline: JSON-LD remote contexts come only from `jsonld.documentLoader`.
|
|
196
|
+
* - Recoverable oddities (named graphs merged, unsupported OBO stanzas, d3 index ids, …) are
|
|
197
|
+
* reported in `warnings` instead of failing.
|
|
198
|
+
*
|
|
199
|
+
* Linear in the input size (plus sorting the output); 50k triples parse in well under a second.
|
|
200
|
+
*/
|
|
201
|
+
export function parseGraph(text: string, options: ParseOptions = {}): ParseResult {
|
|
202
|
+
if (typeof text !== 'string')
|
|
203
|
+
throw invalidInput('parseGraph expects the file content as a string.');
|
|
204
|
+
const requested = options.format ?? 'auto';
|
|
205
|
+
if (requested !== 'auto' && !FORMAT_IDS.has(requested)) {
|
|
206
|
+
throw invalidInput(
|
|
207
|
+
`Unknown format "${String(requested)}". Expected auto, ${[...FORMAT_IDS].join(', ')}.`,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
let format = requested === 'auto' ? detectFormat(text, options.filename) : requested;
|
|
211
|
+
const warnings = new Warnings();
|
|
212
|
+
let parsed: {
|
|
213
|
+
readonly graph: GraphInput;
|
|
214
|
+
readonly prefixes?: Readonly<Record<string, string>>;
|
|
215
|
+
readonly predicateLabels: Readonly<Record<string, string>>;
|
|
216
|
+
readonly dialect?: JsonDialect;
|
|
217
|
+
};
|
|
218
|
+
switch (format) {
|
|
219
|
+
case 'json':
|
|
220
|
+
case 'jsonld': {
|
|
221
|
+
const data = parseJsonText(text);
|
|
222
|
+
// JSON-LD is RDF, also when it arrives as plain `.json`.
|
|
223
|
+
if (format === 'jsonld' || isJsonLd(data)) {
|
|
224
|
+
format = 'jsonld';
|
|
225
|
+
const jsonld = { ...options.jsonld, baseIRI: options.baseIRI };
|
|
226
|
+
parsed = mapRdf(readJsonLd(data, jsonld, warnings), options.rdf, warnings);
|
|
227
|
+
} else parsed = parseJson(data, options.json, warnings);
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
case 'obo':
|
|
231
|
+
parsed = parseObo(text, warnings);
|
|
232
|
+
break;
|
|
233
|
+
case 'turtle':
|
|
234
|
+
case 'ntriples':
|
|
235
|
+
case 'nquads':
|
|
236
|
+
case 'trig':
|
|
237
|
+
parsed = mapRdf(readN3(text, format, options.baseIRI), options.rdf, warnings);
|
|
238
|
+
break;
|
|
239
|
+
case 'rdfxml':
|
|
240
|
+
parsed = mapRdf(readRdfXml(text, options.baseIRI), options.rdf, warnings);
|
|
241
|
+
break;
|
|
242
|
+
default:
|
|
243
|
+
return assertNever(format);
|
|
244
|
+
}
|
|
245
|
+
let { graph } = parsed;
|
|
246
|
+
const { predicateLabels, dialect, prefixes = WELL_KNOWN_PREFIXES } = parsed;
|
|
247
|
+
const hasLabels = Object.keys(predicateLabels).length > 0;
|
|
248
|
+
if (options.filename !== undefined || hasLabels) {
|
|
249
|
+
const meta = makeMeta({
|
|
250
|
+
...(isRecord(graph.meta) ? graph.meta : {}),
|
|
251
|
+
...(hasLabels ? { predicateLabels } : {}),
|
|
252
|
+
...(options.filename !== undefined ? { source: options.filename } : {}),
|
|
253
|
+
});
|
|
254
|
+
graph = { ...graph, ...(meta ? { meta } : {}) };
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
graph,
|
|
258
|
+
format,
|
|
259
|
+
...(dialect !== undefined ? { dialect } : {}),
|
|
260
|
+
prefixes,
|
|
261
|
+
predicateLabels,
|
|
262
|
+
warnings: warnings.finish(),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Best-effort format detection from file name and content sniffing.
|
|
268
|
+
*
|
|
269
|
+
* The extension wins when it is known (`FORMAT_EXTENSIONS`, case-insensitive; query strings
|
|
270
|
+
* are ignored), with two refinements: `.json` holding JSON-LD is `jsonld`, and `.owl` — which
|
|
271
|
+
* Protégé also uses for Turtle, OWL/XML and Functional Syntax — is decided by its content
|
|
272
|
+
* (RDF/XML when that cannot tell). Otherwise: `{` (or a JSON array) → json, or jsonld when a
|
|
273
|
+
* top-level key is `@context`, `@graph`, `@id`, `@type`…; XML → rdfxml for an `rdf:RDF` root
|
|
274
|
+
* (or a root using the RDF namespace); `format-version:` or a `[Term]` / `[Typedef]` /
|
|
275
|
+
* `[Instance]` line → obo; Turtle directives → turtle, or trig when a `{ … }` graph block
|
|
276
|
+
* appears; line-based statements → ntriples (3 terms) or nquads (4 terms). Empty input is an
|
|
277
|
+
* (empty) Turtle graph. Throws `KgdiffError('invalid-input')` with a conversion hint for
|
|
278
|
+
* recognisably unsupported formats (OWL/XML, OWL Functional / Manchester Syntax, GraphML,
|
|
279
|
+
* GEXF, other XML) and for unrecognisable text.
|
|
280
|
+
*/
|
|
281
|
+
export function detectFormat(text: string, filename?: string): FormatId {
|
|
282
|
+
return detect(text, filename);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Compact an IRI with a prefix map: `http://purl.obolibrary.org/obo/GO_0008150` → `GO:0008150`.
|
|
287
|
+
*
|
|
288
|
+
* Uses the well-known prefixes (rdf, rdfs, owl, xsd, skos, schema, dcterms, dc, foaf, prov,
|
|
289
|
+
* oboInOwl, obo) overlaid with `prefixes`; OBO PURLs follow the OBO rule unless `prefixes`
|
|
290
|
+
* binds that id space elsewhere. The longest matching namespace wins; when several names share
|
|
291
|
+
* one namespace, names from `prefixes` win over well-known ones. Returns `iri` unchanged when
|
|
292
|
+
* nothing matches. For display only: node ids keep full IRIs.
|
|
293
|
+
*/
|
|
294
|
+
export function compactIri(iri: string, prefixes?: Readonly<Record<string, string>>): string {
|
|
295
|
+
return tableFor(prefixes).compact(iri);
|
|
296
|
+
}
|
package/src/iri.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IRI references for RDF/XML and JSON-LD: the absolute-IRI test and RFC 3986 §5.2 resolution.
|
|
3
|
+
* Absolute IRIs are kept as written, as n3 does for Turtle, so one resource gets one id
|
|
4
|
+
* whichever syntax names it. Pure string operations; nothing is ever fetched.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9+.-]*:/;
|
|
8
|
+
|
|
9
|
+
/** RFC 3986 appendix B: scheme, authority, path, query, fragment. */
|
|
10
|
+
const PARTS = /^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/s;
|
|
11
|
+
|
|
12
|
+
const DOT_SEGMENT = /(?:^|\/)\.\.?(?:\/|$)/;
|
|
13
|
+
|
|
14
|
+
/** `true` when `value` starts with a scheme (`http:`, `urn:`, `_:` does not count). */
|
|
15
|
+
export function isAbsoluteIri(value: string): boolean {
|
|
16
|
+
return SCHEME.test(value);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves an IRI reference against `base` (RFC 3986 §5.2.2). Absolute references come back
|
|
21
|
+
* unchanged; without a base, relative references stay relative (as in Turtle without `@base`).
|
|
22
|
+
*/
|
|
23
|
+
export function resolveIri(reference: string, base: string | undefined): string {
|
|
24
|
+
if (base === undefined || base === '' || SCHEME.test(reference)) return reference;
|
|
25
|
+
const r = PARTS.exec(reference);
|
|
26
|
+
const b = PARTS.exec(base);
|
|
27
|
+
if (!r || !b) return reference;
|
|
28
|
+
const [, , rAuthority, rPath = '', rQuery, rFragment] = r;
|
|
29
|
+
const [, bScheme, bAuthority, bPath = '', bQuery] = b;
|
|
30
|
+
let authority = bAuthority;
|
|
31
|
+
let path: string;
|
|
32
|
+
let query = rQuery;
|
|
33
|
+
if (rAuthority !== undefined) {
|
|
34
|
+
authority = rAuthority;
|
|
35
|
+
path = removeDotSegments(rPath);
|
|
36
|
+
} else if (rPath === '') {
|
|
37
|
+
path = bPath;
|
|
38
|
+
query = rQuery ?? bQuery;
|
|
39
|
+
} else if (rPath.startsWith('/')) {
|
|
40
|
+
path = removeDotSegments(rPath);
|
|
41
|
+
} else {
|
|
42
|
+
// Merge (§5.2.3): a base with an authority and an empty path merges under "/".
|
|
43
|
+
const merged =
|
|
44
|
+
bAuthority !== undefined && bPath === ''
|
|
45
|
+
? `/${rPath}`
|
|
46
|
+
: bPath.slice(0, bPath.lastIndexOf('/') + 1) + rPath;
|
|
47
|
+
path = removeDotSegments(merged);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
(bScheme !== undefined ? `${bScheme}:` : '') +
|
|
51
|
+
(authority !== undefined ? `//${authority}` : '') +
|
|
52
|
+
path +
|
|
53
|
+
(query !== undefined ? `?${query}` : '') +
|
|
54
|
+
(rFragment !== undefined ? `#${rFragment}` : '')
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** RFC 3986 §5.2.4, literally: `/a/b/../c/./d` → `/a/c/d`. */
|
|
59
|
+
function removeDotSegments(path: string): string {
|
|
60
|
+
if (!DOT_SEGMENT.test(path)) return path;
|
|
61
|
+
let input = path;
|
|
62
|
+
let output = '';
|
|
63
|
+
const dropLastSegment = () => {
|
|
64
|
+
output = output.slice(0, Math.max(0, output.lastIndexOf('/')));
|
|
65
|
+
};
|
|
66
|
+
while (input !== '') {
|
|
67
|
+
if (input.startsWith('../')) input = input.slice(3);
|
|
68
|
+
else if (input.startsWith('./')) input = input.slice(2);
|
|
69
|
+
else if (input.startsWith('/./')) input = input.slice(2);
|
|
70
|
+
else if (input === '/.') input = '/';
|
|
71
|
+
else if (input.startsWith('/../')) {
|
|
72
|
+
input = input.slice(3);
|
|
73
|
+
dropLastSegment();
|
|
74
|
+
} else if (input === '/..') {
|
|
75
|
+
input = '/';
|
|
76
|
+
dropLastSegment();
|
|
77
|
+
} else if (input === '.' || input === '..') input = '';
|
|
78
|
+
else {
|
|
79
|
+
const next = input.indexOf('/', 1);
|
|
80
|
+
const end = next < 0 ? input.length : next;
|
|
81
|
+
output += input.slice(0, end);
|
|
82
|
+
input = input.slice(end);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return output;
|
|
86
|
+
}
|