ochre-sdk 0.20.20 → 0.20.21

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/dist/index.d.mts CHANGED
@@ -1223,7 +1223,6 @@ declare function fetchItem<T extends DataCategory = DataCategory, U extends Data
1223
1223
  *
1224
1224
  * @param params - The parameters for the fetch
1225
1225
  * @param params.setScopeUuids - The Set scope UUIDs to filter by
1226
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
1227
1226
  * @param params.propertyVariableUuids - The property variable UUIDs to filter by
1228
1227
  * @param params.queries - Ordered queries to combine with AND/OR and optional NOT via negation
1229
1228
  * @param params.sort - Optional sorting configuration applied before pagination.
@@ -1238,7 +1237,6 @@ declare function fetchItem<T extends DataCategory = DataCategory, U extends Data
1238
1237
  */
1239
1238
  declare function fetchSetItems<U extends Array<DataCategory> = Array<DataCategory>>(params: {
1240
1239
  setScopeUuids: Array<string>;
1241
- belongsToCollectionScopeUuids: Array<string>;
1242
1240
  propertyVariableUuids: Array<string>;
1243
1241
  queries: Array<Query>;
1244
1242
  sort?: SetItemsSort;
@@ -1267,7 +1265,6 @@ declare function fetchSetItems<U extends Array<DataCategory> = Array<DataCategor
1267
1265
  *
1268
1266
  * @param params - The parameters for the fetch
1269
1267
  * @param params.setScopeUuids - An array of set scope UUIDs to filter by
1270
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
1271
1268
  * @param params.propertyVariableUuids - The property variable UUIDs to query by
1272
1269
  * @param params.queries - Ordered queries to combine with AND/OR and optional NOT via negation
1273
1270
  * @param params.attributes - Whether to return values for bibliographies and periods
@@ -1282,7 +1279,6 @@ declare function fetchSetItems<U extends Array<DataCategory> = Array<DataCategor
1282
1279
  */
1283
1280
  declare function fetchSetPropertyValuesByPropertyVariables(params: {
1284
1281
  setScopeUuids: Array<string>;
1285
- belongsToCollectionScopeUuids: Array<string>;
1286
1282
  propertyVariableUuids: Array<string>;
1287
1283
  queries?: Array<Query>;
1288
1284
  attributes?: {
package/dist/index.mjs CHANGED
@@ -16,6 +16,9 @@ const TEXT_ANNOTATION_TEXT_STYLING_HEADING_LEVEL_UUID = "d4266f0b-3f8d-4b32-8c15
16
16
 
17
17
  //#endregion
18
18
  //#region src/utils/string.ts
19
+ const EMAIL_BRACKET_CLEANUP_REGEX = /(?<=\s|^)[([{]+|[)\]}]+(?=\s|$)/g;
20
+ const EMAIL_PUNCTUATION_CLEANUP_REGEX = /[!),:;?\]]/g;
21
+ const EMAIL_TRAILING_PERIOD_REGEX = /\.$/;
19
22
  /**
20
23
  * Finds a string item in an array by language code
21
24
  *
@@ -46,7 +49,7 @@ function parseEmail(string) {
46
49
  const splitString = string.split(" ");
47
50
  const returnSplitString = [];
48
51
  for (const string of splitString) {
49
- const cleanString = transformPermanentIdentificationUrl(string).replaceAll(/(?<=\s|^)[([{]+|[)\]}]+(?=\s|$)/g, "").replaceAll(/[!),:;?\]]/g, "").replace(/\.$/, "");
52
+ const cleanString = transformPermanentIdentificationUrl(string).replaceAll(EMAIL_BRACKET_CLEANUP_REGEX, "").replaceAll(EMAIL_PUNCTUATION_CLEANUP_REGEX, "").replace(EMAIL_TRAILING_PERIOD_REGEX, "");
50
53
  const index = string.indexOf(cleanString);
51
54
  const before = string.slice(0, index);
52
55
  const after = string.slice(index + cleanString.length);
@@ -442,6 +445,7 @@ function parseStringContent(content, language = "eng") {
442
445
 
443
446
  //#endregion
444
447
  //#region src/utils/internal.ts
448
+ const PSEUDO_UUID_REGEX = /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i;
445
449
  /**
446
450
  * Get the category of an item from the OCHRE API response
447
451
  * @param keys - The keys of the OCHRE API response
@@ -481,7 +485,7 @@ function getItemCategories(keys) {
481
485
  * @internal
482
486
  */
483
487
  function isPseudoUuid(value) {
484
- return /^[\da-f]{8}(?:-[\da-f]{4}){3}-[\da-f]{12}$/i.test(value);
488
+ return PSEUDO_UUID_REGEX.test(value);
485
489
  }
486
490
  /**
487
491
  * Flatten a properties array
@@ -900,6 +904,7 @@ const setItemsParamsSchema = z.object({
900
904
 
901
905
  //#endregion
902
906
  //#region src/utils/parse/index.ts
907
+ const TRAILING_ELLIPSIS_REGEX = /\s*\.{3}$/;
903
908
  /**
904
909
  * Parses raw identification data into the standardized Identification type
905
910
  *
@@ -1406,7 +1411,7 @@ function parseProperty(property, language = "eng") {
1406
1411
  });
1407
1412
  return {
1408
1413
  uuid: property.label.uuid,
1409
- label: parseStringContent(property.label, language).replace(/\s*\.{3}$/, "").trim(),
1414
+ label: parseStringContent(property.label, language).replace(TRAILING_ELLIPSIS_REGEX, "").trim(),
1410
1415
  values,
1411
1416
  comment: property.comment != null ? parseStringContent(property.comment) : null,
1412
1417
  properties: property.property ? parseProperties(ensureArray(property.property)) : []
@@ -2142,10 +2147,18 @@ async function fetchItem(uuid, category, itemCategories, options) {
2142
2147
 
2143
2148
  //#endregion
2144
2149
  //#region src/utils/fetchers/set/query-helpers.ts
2145
- /**
2146
- * Build a string match predicate for an XQuery string
2147
- */
2148
- function buildStringMatchPredicate(params) {
2150
+ const CTS_INCLUDES_STOP_WORDS = [
2151
+ "of",
2152
+ "the",
2153
+ "and",
2154
+ "in",
2155
+ "it"
2156
+ ];
2157
+ const CTS_INCLUDES_STOP_WORDS_VAR = "$ctsIncludesStopWords";
2158
+ /**
2159
+ * Build a string match predicate for an XQuery string.
2160
+ */
2161
+ function buildRawStringMatchPredicate(params) {
2149
2162
  const { path, value, matchMode, isCaseSensitive } = params;
2150
2163
  const comparedPath = isCaseSensitive ? path : `lower-case(${path})`;
2151
2164
  const comparedValueLiteral = stringLiteral(isCaseSensitive ? value : value.toLowerCase());
@@ -2153,6 +2166,77 @@ function buildStringMatchPredicate(params) {
2153
2166
  return `${comparedPath} = ${comparedValueLiteral}`;
2154
2167
  }
2155
2168
  /**
2169
+ * Build CTS word-query options for API v2 includes search.
2170
+ */
2171
+ function buildCtsQueryOptionsExpression(isCaseSensitive) {
2172
+ return `(${[
2173
+ isCaseSensitive ? "case-sensitive" : "case-insensitive",
2174
+ "diacritic-insensitive",
2175
+ "punctuation-insensitive",
2176
+ "whitespace-insensitive",
2177
+ "stemmed"
2178
+ ].map((option) => stringLiteral(option)).join(", ")})`;
2179
+ }
2180
+ /**
2181
+ * Build a CTS-backed includes predicate for an XQuery string.
2182
+ */
2183
+ function buildCtsIncludesPredicate(params) {
2184
+ const { path, value, isCaseSensitive, queryIndex } = params;
2185
+ const searchStringVar = `$query${queryIndex}SearchString`;
2186
+ const rawTermsVar = `$query${queryIndex}RawTerms`;
2187
+ const termsVar = `$query${queryIndex}Terms`;
2188
+ const ctsQueryVar = `$query${queryIndex}CtsQuery`;
2189
+ const ctsOptionsExpression = buildCtsQueryOptionsExpression(isCaseSensitive);
2190
+ const fallbackPredicate = buildRawStringMatchPredicate({
2191
+ path,
2192
+ value,
2193
+ matchMode: "includes",
2194
+ isCaseSensitive
2195
+ });
2196
+ return {
2197
+ declarations: [
2198
+ `let ${searchStringVar} := ${stringLiteral(value)}`,
2199
+ String.raw`let ${rawTermsVar} := fn:tokenize(${searchStringVar}, "\W+")`,
2200
+ `let ${termsVar} :=
2201
+ for $term in ${rawTermsVar}
2202
+ let $normalizedTerm := fn:lower-case($term)
2203
+ where $normalizedTerm ne "" and not($normalizedTerm = ${CTS_INCLUDES_STOP_WORDS_VAR})
2204
+ return ${isCaseSensitive ? "$term" : "$normalizedTerm"}`,
2205
+ `let ${ctsQueryVar} :=
2206
+ if (count(${termsVar}) = 1)
2207
+ then cts:word-query(${termsVar}[1], ${ctsOptionsExpression})
2208
+ else if (count(${termsVar}) gt 1)
2209
+ then cts:near-query((
2210
+ for $term in ${termsVar}
2211
+ return cts:word-query($term, ${ctsOptionsExpression})
2212
+ ), 5, ("unordered"))
2213
+ else ()`
2214
+ ],
2215
+ predicate: `(if (exists(${ctsQueryVar})) then cts:contains(${path}, ${ctsQueryVar}) else ${fallbackPredicate})`
2216
+ };
2217
+ }
2218
+ /**
2219
+ * Build a string match predicate for an XQuery string.
2220
+ */
2221
+ function buildStringMatchPredicate(params) {
2222
+ const { path, value, matchMode, isCaseSensitive, version, queryIndex } = params;
2223
+ if (matchMode === "includes" && version === 2) return buildCtsIncludesPredicate({
2224
+ path,
2225
+ value,
2226
+ isCaseSensitive,
2227
+ queryIndex
2228
+ });
2229
+ return {
2230
+ declarations: [],
2231
+ predicate: buildRawStringMatchPredicate({
2232
+ path,
2233
+ value,
2234
+ matchMode,
2235
+ isCaseSensitive
2236
+ })
2237
+ };
2238
+ }
2239
+ /**
2156
2240
  * Build a date/dateTime range predicate for an XQuery string.
2157
2241
  */
2158
2242
  function buildDateRangePredicate(params) {
@@ -2163,76 +2247,136 @@ function buildDateRangePredicate(params) {
2163
2247
  return conditions.join(" and ");
2164
2248
  }
2165
2249
  /**
2166
- * Build a property value predicate for an XQuery string
2250
+ * Build a property value predicate for an XQuery string.
2167
2251
  */
2168
- function buildPropertyValuePredicate(query) {
2169
- if (query.dataType === "IDREF") return `.//properties//property[value[@uuid=${stringLiteral(query.value)}]]`;
2170
- if (query.dataType === "date" || query.dataType === "dateTime") return `.//properties//property[(label/@uuid=${stringLiteral(query.value)}) and ${buildDateRangePredicate({
2171
- from: query.from,
2172
- to: query.to
2173
- })}]`;
2174
- if (query.dataType === "time" || query.dataType === "integer" || query.dataType === "decimal" || query.dataType === "boolean") return `.//properties//property[value[@rawValue=${stringLiteral(query.value)}]]`;
2175
- return `.//properties//property[${buildStringMatchPredicate({
2252
+ function buildPropertyValuePredicate(params) {
2253
+ const { query, version, queryIndex } = params;
2254
+ if (query.dataType === "IDREF") return {
2255
+ declarations: [],
2256
+ predicate: `.//properties//property[value[@uuid=${stringLiteral(query.value)}]]`
2257
+ };
2258
+ if (query.dataType === "date" || query.dataType === "dateTime") return {
2259
+ declarations: [],
2260
+ predicate: `.//properties//property[(label/@uuid=${stringLiteral(query.value)}) and ${buildDateRangePredicate({
2261
+ from: query.from,
2262
+ to: query.to
2263
+ })}]`
2264
+ };
2265
+ if (query.dataType === "time" || query.dataType === "integer" || query.dataType === "decimal" || query.dataType === "boolean") return {
2266
+ declarations: [],
2267
+ predicate: `.//properties//property[value[@rawValue=${stringLiteral(query.value)}]]`
2268
+ };
2269
+ const compiledStringPredicate = buildStringMatchPredicate({
2176
2270
  path: `string-join(value/content[@xml:lang="${query.language}"]/string, "")`,
2177
2271
  value: query.value,
2178
2272
  matchMode: query.matchMode,
2179
- isCaseSensitive: query.isCaseSensitive
2180
- })}]`;
2273
+ isCaseSensitive: query.isCaseSensitive,
2274
+ version,
2275
+ queryIndex
2276
+ });
2277
+ return {
2278
+ declarations: compiledStringPredicate.declarations,
2279
+ predicate: `.//properties//property[${compiledStringPredicate.predicate}]`
2280
+ };
2181
2281
  }
2182
2282
  /**
2183
- * Build a query predicate for an XQuery string
2283
+ * Build a query predicate for an XQuery string.
2184
2284
  */
2185
- function buildQueryPredicate(query) {
2285
+ function buildQueryPredicate(params) {
2286
+ const { query, version, queryIndex } = params;
2186
2287
  switch (query.target) {
2187
2288
  case "title": return buildStringMatchPredicate({
2188
2289
  path: `string-join(identification/label/content[@xml:lang="${query.language}"]/string, "")`,
2189
2290
  value: query.value,
2190
2291
  matchMode: query.matchMode,
2191
- isCaseSensitive: query.isCaseSensitive
2292
+ isCaseSensitive: query.isCaseSensitive,
2293
+ version,
2294
+ queryIndex
2192
2295
  });
2193
2296
  case "description": return buildStringMatchPredicate({
2194
2297
  path: `string-join(description/content[@xml:lang="${query.language}"]/string, "")`,
2195
2298
  value: query.value,
2196
2299
  matchMode: query.matchMode,
2197
- isCaseSensitive: query.isCaseSensitive
2300
+ isCaseSensitive: query.isCaseSensitive,
2301
+ version,
2302
+ queryIndex
2198
2303
  });
2199
2304
  case "periods": return buildStringMatchPredicate({
2200
2305
  path: `string-join(periods/period/identification/label/content[@xml:lang="${query.language}"]/string, "")`,
2201
2306
  value: query.value,
2202
2307
  matchMode: query.matchMode,
2203
- isCaseSensitive: query.isCaseSensitive
2308
+ isCaseSensitive: query.isCaseSensitive,
2309
+ version,
2310
+ queryIndex
2204
2311
  });
2205
2312
  case "bibliography": return buildStringMatchPredicate({
2206
2313
  path: `string-join(bibliographies/bibliography/identification/label/content[@xml:lang="${query.language}"]/string, "")`,
2207
2314
  value: query.value,
2208
2315
  matchMode: query.matchMode,
2209
- isCaseSensitive: query.isCaseSensitive
2316
+ isCaseSensitive: query.isCaseSensitive,
2317
+ version,
2318
+ queryIndex
2210
2319
  });
2211
2320
  case "image": return buildStringMatchPredicate({
2212
2321
  path: `string-join(image/identification/label/content[@xml:lang="${query.language}"]/string, "")`,
2213
2322
  value: query.value,
2214
2323
  matchMode: query.matchMode,
2215
- isCaseSensitive: query.isCaseSensitive
2324
+ isCaseSensitive: query.isCaseSensitive,
2325
+ version,
2326
+ queryIndex
2327
+ });
2328
+ case "propertyValue": return buildPropertyValuePredicate({
2329
+ query,
2330
+ version,
2331
+ queryIndex
2216
2332
  });
2217
- case "propertyValue": return buildPropertyValuePredicate(query);
2218
2333
  }
2219
2334
  }
2220
2335
  /**
2221
2336
  * Build a boolean query clause for an XQuery string.
2222
2337
  */
2223
- function buildBooleanQueryClause(query) {
2224
- const baseClause = `(${buildQueryPredicate(query)})`;
2225
- return query.isNegated ? `not(${baseClause})` : baseClause;
2338
+ function buildBooleanQueryClause(params) {
2339
+ const { query, version, queryIndex } = params;
2340
+ const compiledQueryPredicate = buildQueryPredicate({
2341
+ query,
2342
+ version,
2343
+ queryIndex
2344
+ });
2345
+ const baseClause = `(${compiledQueryPredicate.predicate})`;
2346
+ return {
2347
+ declarations: compiledQueryPredicate.declarations,
2348
+ predicate: query.isNegated ? `not(${baseClause})` : baseClause
2349
+ };
2226
2350
  }
2227
2351
  /**
2228
2352
  * Build query filters for an XQuery string.
2229
2353
  */
2230
- function buildQueryFilters(queries) {
2231
- return queries.map((query, index) => {
2232
- const clause = buildBooleanQueryClause(query);
2233
- if (index === 0) return clause;
2234
- return `${query.operator === "AND" ? "and" : "or"} ${clause}`;
2235
- }).join(" ");
2354
+ function buildQueryFilters(params) {
2355
+ const { queries, version } = params;
2356
+ const declarations = [];
2357
+ const predicateParts = [];
2358
+ let hasCtsIncludesClauses = false;
2359
+ for (const [index, query] of queries.entries()) {
2360
+ const compiledClause = buildBooleanQueryClause({
2361
+ query,
2362
+ version,
2363
+ queryIndex: index + 1
2364
+ });
2365
+ if (compiledClause.declarations.length > 0) {
2366
+ hasCtsIncludesClauses = true;
2367
+ declarations.push(...compiledClause.declarations);
2368
+ }
2369
+ if (index === 0) {
2370
+ predicateParts.push(compiledClause.predicate);
2371
+ continue;
2372
+ }
2373
+ predicateParts.push(`${query.operator === "AND" ? "and" : "or"} ${compiledClause.predicate}`);
2374
+ }
2375
+ if (hasCtsIncludesClauses) declarations.unshift(`let ${CTS_INCLUDES_STOP_WORDS_VAR} := (${CTS_INCLUDES_STOP_WORDS.map((stopWord) => stringLiteral(stopWord)).join(", ")})`);
2376
+ return {
2377
+ declarations,
2378
+ predicate: predicateParts.join(" ")
2379
+ };
2236
2380
  }
2237
2381
 
2238
2382
  //#endregion
@@ -2328,7 +2472,11 @@ function buildXQuery$1(params, options) {
2328
2472
  const startPosition = (page - 1) * pageSize + 1;
2329
2473
  const endPosition = page * pageSize;
2330
2474
  const setScopeFilter = `/set[(${setScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]/items/*`;
2331
- const queryFilters = buildQueryFilters(queries);
2475
+ const compiledQueryFilters = buildQueryFilters({
2476
+ queries,
2477
+ version
2478
+ });
2479
+ const queryFilterDeclarations = compiledQueryFilters.declarations.length > 0 ? `${compiledQueryFilters.declarations.join("\n")}\n\n` : "";
2332
2480
  const filterPredicates = [];
2333
2481
  if (belongsToCollectionScopeUuids.length > 0) {
2334
2482
  const belongsToCollectionScopeValues = belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
@@ -2338,10 +2486,10 @@ function buildXQuery$1(params, options) {
2338
2486
  const propertyVariables = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2339
2487
  filterPredicates.push(`.//properties//property[label[${propertyVariables}]]`);
2340
2488
  }
2341
- if (queryFilters.length > 0) filterPredicates.push(`(${queryFilters})`);
2489
+ if (compiledQueryFilters.predicate.length > 0) filterPredicates.push(`(${compiledQueryFilters.predicate})`);
2342
2490
  const itemFilters = filterPredicates.length > 0 ? `[${filterPredicates.join(" and ")}]` : "";
2343
2491
  const orderedItemsClause = buildOrderedItemsClause(sort);
2344
- return `<ochre>{${`let $items := ${version === 2 ? "doc()" : "input()"}/ochre
2492
+ return `<ochre>{${`${queryFilterDeclarations}let $items := ${version === 2 ? "doc()" : "input()"}/ochre
2345
2493
  ${setScopeFilter}
2346
2494
  ${itemFilters}
2347
2495
 
@@ -2360,7 +2508,6 @@ function buildXQuery$1(params, options) {
2360
2508
  *
2361
2509
  * @param params - The parameters for the fetch
2362
2510
  * @param params.setScopeUuids - The Set scope UUIDs to filter by
2363
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
2364
2511
  * @param params.propertyVariableUuids - The property variable UUIDs to filter by
2365
2512
  * @param params.queries - Ordered queries to combine with AND/OR and optional NOT via negation
2366
2513
  * @param params.sort - Optional sorting configuration applied before pagination.
@@ -2622,13 +2769,17 @@ function buildXQuery(params, options) {
2622
2769
  let setScopeFilter = "/set/items/*";
2623
2770
  if (setScopeUuids.length > 0) setScopeFilter = `/set[(${setScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ")})]/items/*`;
2624
2771
  const propertyVariableFilters = propertyVariableUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2625
- const queryFilters = buildQueryFilters(queries);
2772
+ const compiledQueryFilters = buildQueryFilters({
2773
+ queries,
2774
+ version
2775
+ });
2776
+ const queryFilterDeclarations = compiledQueryFilters.declarations.length > 0 ? `${compiledQueryFilters.declarations.join("\n")}\n\n` : "";
2626
2777
  const filterPredicates = [];
2627
2778
  if (belongsToCollectionScopeUuids.length > 0) {
2628
2779
  const belongsToCollectionScopeValues = belongsToCollectionScopeUuids.map((uuid) => `@uuid="${uuid}"`).join(" or ");
2629
2780
  filterPredicates.push(`.//properties[property[label/@uuid="${BELONGS_TO_COLLECTION_UUID}" and value[${belongsToCollectionScopeValues}]]]`);
2630
2781
  }
2631
- if (queryFilters.length > 0) filterPredicates.push(`(${queryFilters})`);
2782
+ if (compiledQueryFilters.predicate.length > 0) filterPredicates.push(`(${compiledQueryFilters.predicate})`);
2632
2783
  const itemFilters = filterPredicates.length > 0 ? `[${filterPredicates.join(" and ")}]` : "";
2633
2784
  const queryBlocks = [`let $matching-props := $items//property[label[${propertyVariableFilters}]]
2634
2785
 
@@ -2659,7 +2810,7 @@ let $property-values :=
2659
2810
  return <attributeValue attributeType="periods" itemUuid="{$item/@uuid}" content="{$label}" />`);
2660
2811
  returnedSequences.push("$period-values");
2661
2812
  }
2662
- return `<ochre>{${`let $items := ${version === 2 ? "doc()" : "input()"}/ochre
2813
+ return `<ochre>{${`${queryFilterDeclarations}let $items := ${version === 2 ? "doc()" : "input()"}/ochre
2663
2814
  ${setScopeFilter}
2664
2815
  ${itemFilters}
2665
2816
 
@@ -2672,7 +2823,6 @@ return (${returnedSequences.join(", ")})`}}</ochre>`;
2672
2823
  *
2673
2824
  * @param params - The parameters for the fetch
2674
2825
  * @param params.setScopeUuids - An array of set scope UUIDs to filter by
2675
- * @param params.belongsToCollectionScopeUuids - The collection scope UUIDs to filter by
2676
2826
  * @param params.propertyVariableUuids - The property variable UUIDs to query by
2677
2827
  * @param params.queries - Ordered queries to combine with AND/OR and optional NOT via negation
2678
2828
  * @param params.attributes - Whether to return values for bibliographies and periods
@@ -3006,6 +3156,8 @@ function filterProperties(property, filter, options = DEFAULT_OPTIONS) {
3006
3156
 
3007
3157
  //#endregion
3008
3158
  //#region src/utils/parse/website.ts
3159
+ const SEGMENT_UNIQUE_SLUG_PREFIX_REGEX = /^\$[^-]*-/;
3160
+ const TRAILING_SLASH_REGEX = /\/$/;
3009
3161
  /**
3010
3162
  * Extracts CSS style properties for a given presentation variant.
3011
3163
  *
@@ -3726,13 +3878,13 @@ function parseWebpage(webpageResource, slugPrefix) {
3726
3878
  const webpageProperties = webpageResource.properties ? parseProperties(ensureArray(webpageResource.properties.property)) : [];
3727
3879
  if (webpageProperties.length === 0 || getPropertyValueByLabel(webpageProperties, "presentation") !== "page") return null;
3728
3880
  const identification = parseIdentification(webpageResource.identification);
3729
- const slug = webpageResource.slug?.replace(/^\$[^-]*-/, "") ?? null;
3881
+ const slug = webpageResource.slug?.replace(SEGMENT_UNIQUE_SLUG_PREFIX_REGEX, "") ?? null;
3730
3882
  if (slug == null) throw new Error(`Slug not found for page “${identification.label}”`);
3731
3883
  const returnWebpage = {
3732
3884
  uuid: webpageResource.uuid,
3733
3885
  type: "page",
3734
3886
  title: identification.label,
3735
- slug: slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug,
3887
+ slug: slugPrefix != null ? `${slugPrefix}/${slug}`.replace(TRAILING_SLASH_REGEX, "") : slug,
3736
3888
  publicationDateTime: parseOptionalDate(webpageResource.publicationDateTime),
3737
3889
  items: [],
3738
3890
  properties: {
@@ -3831,7 +3983,7 @@ function parseWebSegment(segmentResource, slugPrefix) {
3831
3983
  publicationDateTime: parseOptionalDate(segmentResource.publicationDateTime),
3832
3984
  items: []
3833
3985
  };
3834
- returnSegment.items = parseWebSegmentItems(segmentResource.resource ? ensureArray(segmentResource.resource) : [], slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug);
3986
+ returnSegment.items = parseWebSegmentItems(segmentResource.resource ? ensureArray(segmentResource.resource) : [], slugPrefix != null ? `${slugPrefix}/${slug}`.replace(TRAILING_SLASH_REGEX, "") : slug);
3835
3987
  return returnSegment;
3836
3988
  }
3837
3989
  /**
@@ -3869,7 +4021,7 @@ function parseWebSegmentItem(segmentItemResource, slugPrefix) {
3869
4021
  items: []
3870
4022
  };
3871
4023
  const resources = segmentItemResource.resource ? ensureArray(segmentItemResource.resource) : [];
3872
- returnSegmentItem.items.push(...parseWebpages(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug), ...parseSegments(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(/\/$/, "") : slug));
4024
+ returnSegmentItem.items.push(...parseWebpages(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(TRAILING_SLASH_REGEX, "") : slug), ...parseSegments(resources, slugPrefix != null ? `${slugPrefix}/${slug}`.replace(TRAILING_SLASH_REGEX, "") : slug));
3873
4025
  return returnSegmentItem;
3874
4026
  }
3875
4027
  /**
@@ -4275,6 +4427,7 @@ function parseWebsite(websiteTree, metadata, belongsTo, { version = DEFAULT_API_
4275
4427
 
4276
4428
  //#endregion
4277
4429
  //#region src/utils/fetchers/website.ts
4430
+ const API_VERSION_SUFFIX_REGEX = /-v\d+$/;
4278
4431
  /**
4279
4432
  * Parses the version suffix from an API abbreviation
4280
4433
  *
@@ -4282,7 +4435,7 @@ function parseWebsite(websiteTree, metadata, belongsTo, { version = DEFAULT_API_
4282
4435
  * @returns The parsed abbreviation and API version
4283
4436
  */
4284
4437
  function parseApiVersionSuffix(abbreviation) {
4285
- if (!/-v\d+$/.test(abbreviation)) return {
4438
+ if (!API_VERSION_SUFFIX_REGEX.test(abbreviation)) return {
4286
4439
  abbreviation,
4287
4440
  version: DEFAULT_API_VERSION
4288
4441
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ochre-sdk",
3
- "version": "0.20.20",
3
+ "version": "0.20.21",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Node.js library for working with OCHRE (Online Cultural and Historical Research Environment) data",
@@ -46,8 +46,8 @@
46
46
  "zod": "^4.3.6"
47
47
  },
48
48
  "devDependencies": {
49
- "@antfu/eslint-config": "^7.6.1",
50
- "@types/node": "^24.11.0",
49
+ "@antfu/eslint-config": "^7.7.0",
50
+ "@types/node": "^24.12.0",
51
51
  "bumpp": "^10.4.1",
52
52
  "eslint": "^10.0.2",
53
53
  "prettier": "^3.8.1",