@visulima/string 2.0.6 → 3.0.0-alpha.2

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/LICENSE.md +374 -0
  3. package/README.md +57 -22
  4. package/dist/case/camel-case.d.ts +2 -2
  5. package/dist/case/camel-case.js +2 -2
  6. package/dist/case/capital-case.d.ts +2 -2
  7. package/dist/case/capital-case.js +2 -2
  8. package/dist/case/constant-case.d.ts +2 -2
  9. package/dist/case/dot-case.d.ts +2 -2
  10. package/dist/case/flat-case.d.ts +2 -2
  11. package/dist/case/flip-case.d.ts +2 -2
  12. package/dist/case/flip-case.js +8 -2
  13. package/dist/case/identify-case.d.ts +2 -2
  14. package/dist/case/index.d.ts +2 -2
  15. package/dist/case/index.js +1 -1
  16. package/dist/case/kebab-case.d.ts +2 -2
  17. package/dist/case/kebab-case.js +2 -2
  18. package/dist/case/lower-first.d.ts +2 -2
  19. package/dist/case/no-case.d.ts +2 -2
  20. package/dist/case/pascal-case.d.ts +2 -2
  21. package/dist/case/pascal-case.js +2 -2
  22. package/dist/case/pascal-snake-case.d.ts +2 -2
  23. package/dist/case/path-case.d.ts +2 -2
  24. package/dist/case/sentence-case.d.ts +2 -2
  25. package/dist/case/sentence-case.js +2 -2
  26. package/dist/case/snake-case.d.ts +2 -2
  27. package/dist/case/split-by-case.d.ts +2 -2
  28. package/dist/case/split-by-case.js +127 -24
  29. package/dist/case/title-case.d.ts +2 -2
  30. package/dist/case/train-case.d.ts +2 -2
  31. package/dist/case/train-case.js +1 -1
  32. package/dist/case/upper-first.d.ts +2 -2
  33. package/dist/closest-string.js +1 -3
  34. package/dist/get-string-truncated-width.js +1 -1
  35. package/dist/index.d.ts +11 -6
  36. package/dist/index.js +5 -4
  37. package/dist/native-string-types.d.ts +1 -1
  38. package/dist/outdent.js +3 -3
  39. package/dist/packem_shared/closestN-Cvsw0Fue.js +24 -0
  40. package/dist/packem_shared/{constants-CTzC6Gjg.js → constants-CnXdp6qx.js} +1 -1
  41. package/dist/packem_shared/excerpt-GvmR4ESb.js +1502 -0
  42. package/dist/packem_shared/{joinSegments-CuU8RGpP.js → joinSegments-B4H6oZ9b.js} +1 -1
  43. package/dist/packem_shared/{replaceString-vYvRBMfC.js → replaceString-BVlxk27d.js} +8 -8
  44. package/dist/packem_shared/{transliterate-CYnKKT8f.js → transliterate-a6LLM_Ei.js} +3 -5
  45. package/dist/packem_shared/{types-CfPsw9ix.d.ts → types-CHpmpy8C.d.ts} +1 -1
  46. package/dist/packem_shared/{types-Daae0hGf.d.ts → types-D2ydhHCv.d.ts} +1 -1
  47. package/dist/slice.js +1 -1
  48. package/dist/slugify.d.ts +1 -1
  49. package/dist/slugify.js +1 -1
  50. package/dist/truncate.js +38 -13
  51. package/dist/utilities.d.ts +1 -1
  52. package/dist/word-wrap.js +18 -18
  53. package/package.json +3 -3
  54. package/dist/packem_shared/closestN-BREy4E7C.js +0 -19
@@ -1,4 +1,4 @@
1
- import { R as RE_FAST_ANSI } from './constants-CTzC6Gjg.js';
1
+ import { R as RE_FAST_ANSI } from './constants-CnXdp6qx.js';
2
2
 
3
3
  const joinSegments = (segments, joiner) => {
4
4
  const { length } = segments;
@@ -99,17 +99,17 @@ const replaceString = (source, searches, ignoreRanges) => {
99
99
  }
100
100
  return b.end - a.end;
101
101
  });
102
- const sortedIgnores = [...ignoreRanges].sort((a, b) => a[0] - b[0]);
102
+ const sortedIgnores = ignoreRanges.toSorted((a, b) => a[0] - b[0]);
103
103
  const mergedIgnores = mergeIntervals(sortedIgnores);
104
104
  const processedChars = source.split("").map((char, index) => {
105
105
  return {
106
- appliedMatchId: null,
106
+ appliedMatchId: void 0,
107
107
  char,
108
108
  index,
109
- insertBeforeReplacement: null,
109
+ insertBeforeReplacement: void 0,
110
110
  isIgnored: false,
111
111
  isMatchStart: false,
112
- matchReplacement: null
112
+ matchReplacement: void 0
113
113
  };
114
114
  });
115
115
  for (const range of mergedIgnores) {
@@ -139,7 +139,7 @@ const replaceString = (source, searches, ignoreRanges) => {
139
139
  }
140
140
  let canApply = true;
141
141
  for (let index = match.start; index <= match.end; index++) {
142
- if (!processedChars[index] || processedChars[index].isIgnored || processedChars[index].appliedMatchId !== null) {
142
+ if (!processedChars[index] || processedChars[index].isIgnored || processedChars[index].appliedMatchId !== void 0) {
143
143
  canApply = false;
144
144
  break;
145
145
  }
@@ -160,10 +160,10 @@ const replaceString = (source, searches, ignoreRanges) => {
160
160
  let currentIndex = 0;
161
161
  while (currentIndex < processedChars.length) {
162
162
  const pChar = processedChars[currentIndex];
163
- if (pChar.insertBeforeReplacement !== null) {
163
+ if (pChar.insertBeforeReplacement !== void 0) {
164
164
  result += pChar.insertBeforeReplacement;
165
165
  }
166
- if (pChar.isMatchStart && pChar.appliedMatchId !== null) {
166
+ if (pChar.isMatchStart && pChar.appliedMatchId !== void 0) {
167
167
  result += pChar.matchReplacement ?? "";
168
168
  let matchEndIndex = currentIndex;
169
169
  for (let index = currentIndex + 1; index < processedChars.length; index++) {
@@ -174,7 +174,7 @@ const replaceString = (source, searches, ignoreRanges) => {
174
174
  }
175
175
  }
176
176
  currentIndex = matchEndIndex + 1;
177
- } else if (pChar.appliedMatchId === null) {
177
+ } else if (pChar.appliedMatchId === void 0) {
178
178
  result += pChar.char;
179
179
  currentIndex += 1;
180
180
  } else {
@@ -1,5 +1,5 @@
1
- import { G as RE_THAI } from './constants-CTzC6Gjg.js';
2
- import replaceString from './replaceString-vYvRBMfC.js';
1
+ import { F as RE_THAI } from './constants-CnXdp6qx.js';
2
+ import replaceString from './replaceString-BVlxk27d.js';
3
3
  import { findStringOccurrences, hasPunctuationOrSpace, hasChinese } from '../utilities.js';
4
4
 
5
5
  const UNICODE_BLOCK_0000_00FF = [
@@ -47618,7 +47618,7 @@ const formatReplaceOption = (option) => {
47618
47618
  return structuredClone(option);
47619
47619
  }
47620
47620
  const replaceArray = [];
47621
- for (const key in option) {
47621
+ for (const key of Object.keys(option)) {
47622
47622
  if (Object.prototype.hasOwnProperty.call(option, key)) {
47623
47623
  const value = option[key];
47624
47624
  if (typeof value === "string") {
@@ -47687,8 +47687,6 @@ const transliterate = (source, options) => {
47687
47687
  const codePoint = input.codePointAt(index);
47688
47688
  if (codePoint === void 0) {
47689
47689
  char = input[index];
47690
- charLength = 1;
47691
- s = opt.unknown;
47692
47690
  } else {
47693
47691
  char = String.fromCodePoint(codePoint);
47694
47692
  charLength = char.length;
@@ -1,4 +1,4 @@
1
- import { N as NodeLocale, k as IsStringLiteral } from './types-Daae0hGf.js';
1
+ import { N as NodeLocale, k as IsStringLiteral } from './types-D2ydhHCv.js';
2
2
  import { L as LRUCache } from './lru-cache-B_t27XwI.js';
3
3
 
4
4
  type SplitterCharacter = " " | "_" | "-" | "." | "/";
@@ -14,7 +14,7 @@ type IsNumberLiteral<T extends number> = [T] extends [number] ? ([number] extend
14
14
  type IsBooleanLiteral<T extends boolean> = [T] extends [boolean] ? ([boolean] extends [T] ? false : true) : false;
15
15
  type Reverse<T extends string, Accumulator extends string = ""> = T extends `${infer Head}${infer Tail}` ? Reverse<Tail, `${Head}${Accumulator}`> : Accumulator extends "" ? T : `${T}${Accumulator}`;
16
16
  type Any<BoolArray extends boolean[]> = BoolArray extends [infer Head extends boolean, ...infer Rest extends boolean[]] ? IsBooleanLiteral<Head> extends true ? Head extends true ? true : Any<Rest> : Any<Rest> : false;
17
- type All<BoolArray extends boolean[]> = IsBooleanLiteral<BoolArray[number]> extends true ? BoolArray extends [infer Head extends boolean, ...infer Rest extends boolean[]] ? Head extends true ? Any<Rest> : false : true : false;
17
+ type All<BoolArray extends boolean[]> = IsBooleanLiteral<BoolArray[number]> extends true ? BoolArray extends [infer Head extends boolean, ...infer Rest extends boolean[]] ? Head extends true ? All<Rest> : false : true : false;
18
18
  type IsStringLiteral<T extends string> = [T] extends [string] ? [string] extends [T] ? false : Uppercase<T> extends Uppercase<Lowercase<T>> ? Lowercase<T> extends Lowercase<Uppercase<T>> ? true : false : false : false;
19
19
  type IsStringLiteralArray<StringArray extends ReadonlyArray<string>> = IsStringLiteral<StringArray[number]> extends true ? true : false;
20
20
  type CharAt<T extends string, Index extends number> = All<[IsStringLiteral<T>, IsNumberLiteral<Index>]> extends true ? Split<T>[Index] : string;
package/dist/slice.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as RE_VALID_ANSI_PAIRS, b as RE_VALID_HYPERLINKS, c as RE_ANSI } from './packem_shared/constants-CTzC6Gjg.js';
1
+ import { g as RE_VALID_ANSI_PAIRS, h as RE_VALID_HYPERLINKS, i as RE_ANSI } from './packem_shared/constants-CnXdp6qx.js';
2
2
  import { getStringWidth } from './get-string-width.js';
3
3
  import LRUCache from './packem_shared/LRUCache-3r3SovvR.js';
4
4
 
package/dist/slugify.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { j as SlugifyOptions } from './packem_shared/types-Daae0hGf.js';
1
+ import { j as SlugifyOptions } from './packem_shared/types-D2ydhHCv.js';
2
2
 
3
3
  declare const slugify: (input: string, options?: SlugifyOptions) => string;
4
4
 
package/dist/slugify.js CHANGED
@@ -1,4 +1,4 @@
1
- import transliterate from './packem_shared/transliterate-CYnKKT8f.js';
1
+ import transliterate from './packem_shared/transliterate-a6LLM_Ei.js';
2
2
  import { escapeRegExp } from './utilities.js';
3
3
 
4
4
  const removeDisallowedChars = (input, allowedChars, separator) => {
package/dist/truncate.js CHANGED
@@ -1,22 +1,33 @@
1
1
  import { getStringTruncatedWidth } from './get-string-truncated-width.js';
2
+ import { getStringWidth } from './get-string-width.js';
2
3
  import { slice } from './slice.js';
3
4
 
4
5
  const DEFAULT_ELLIPSIS = "…";
5
6
  const MAX_SPACE_SEARCH_DISTANCE = 3;
6
- const findNearestSpace = (string_, startIndex, searchRight = false) => {
7
- if (string_.charAt(startIndex) === " ") {
7
+ const findNearestSpace = (value, startIndex, searchRight = false) => {
8
+ if (value.charAt(startIndex) === " ") {
8
9
  return startIndex;
9
10
  }
10
11
  const direction = searchRight ? 1 : -1;
11
- const limit = Math.min(MAX_SPACE_SEARCH_DISTANCE, searchRight ? string_.length - startIndex : startIndex);
12
+ const limit = Math.min(MAX_SPACE_SEARCH_DISTANCE, searchRight ? value.length - startIndex : startIndex);
12
13
  for (let offset = 1; offset <= limit; offset++) {
13
14
  const index = startIndex + offset * direction;
14
- if (string_.charAt(index) === " ") {
15
+ if (value.charAt(index) === " ") {
15
16
  return index;
16
17
  }
17
18
  }
18
19
  return startIndex;
19
20
  };
21
+ const widthToIndex = (input, targetWidth, widthOptions) => {
22
+ const result = getStringTruncatedWidth(input, {
23
+ ...widthOptions,
24
+ ellipsis: "",
25
+ ellipsisWidth: 0,
26
+ limit: targetWidth
27
+ });
28
+ return result.index;
29
+ };
30
+ const indexToWidth = (input, stringIndex, widthOptions) => getStringWidth(input.slice(0, stringIndex), widthOptions);
20
31
  const truncate = (input, limit, options = {}) => {
21
32
  if (typeof input !== "string") {
22
33
  throw new TypeError(`Expected \`input\` to be a string, got ${typeof input}`);
@@ -49,8 +60,11 @@ const truncate = (input, limit, options = {}) => {
49
60
  switch (position) {
50
61
  case "end": {
51
62
  if (preferTruncationOnSpace) {
52
- const nearestSpace = findNearestSpace(input, limit - 1);
53
- return slice(input, 0, nearestSpace, {
63
+ const targetWidth = limit - ellipsisWidth;
64
+ const stringIndex = widthToIndex(input, targetWidth, options.width);
65
+ const nearestSpace = findNearestSpace(input, stringIndex);
66
+ const nearestSpaceWidth = indexToWidth(input, nearestSpace, options.width);
67
+ return slice(input, 0, nearestSpaceWidth, {
54
68
  width: options.width
55
69
  }) + ellipsis;
56
70
  }
@@ -60,21 +74,32 @@ const truncate = (input, limit, options = {}) => {
60
74
  }
61
75
  case "middle": {
62
76
  const half = Math.floor(limit / 2);
77
+ const totalTextWidth = Math.max(0, limit - ellipsisWidth);
78
+ const leftWidth = Math.min(half, totalTextWidth);
79
+ const rightWidth = Math.max(0, totalTextWidth - leftWidth);
63
80
  if (preferTruncationOnSpace) {
64
- const firstBreak = findNearestSpace(input, half);
65
- const secondBreak = findNearestSpace(input, width - (limit - half) + 1, true);
66
- return slice(input, 0, firstBreak) + ellipsis + slice(input, secondBreak, width).trim();
81
+ const firstBreakIndex = widthToIndex(input, leftWidth, options.width);
82
+ const firstBreak = findNearestSpace(input, firstBreakIndex);
83
+ const firstBreakWidth = indexToWidth(input, firstBreak, options.width);
84
+ const secondBreakWidthTarget = width - rightWidth;
85
+ const secondBreakIndex = widthToIndex(input, secondBreakWidthTarget, options.width);
86
+ const secondBreak = findNearestSpace(input, secondBreakIndex, true);
87
+ const secondBreakWidthActual = indexToWidth(input, secondBreak, options.width);
88
+ return slice(input, 0, firstBreakWidth, { width: options.width }) + ellipsis + slice(input, secondBreakWidthActual, width, { width: options.width }).trim();
67
89
  }
68
- return slice(input, 0, half, {
90
+ return slice(input, 0, leftWidth, {
69
91
  width: options.width
70
- }) + ellipsis + slice(input, width - (limit - half) + ellipsisWidth, width, {
92
+ }) + ellipsis + slice(input, width - rightWidth, width, {
71
93
  width: options.width
72
94
  });
73
95
  }
74
96
  case "start": {
75
97
  if (preferTruncationOnSpace) {
76
- const nearestSpace = findNearestSpace(input, width - limit + 1, true);
77
- return ellipsis + slice(input, nearestSpace, width).trim();
98
+ const targetWidth = width - limit + ellipsisWidth;
99
+ const stringIndex = widthToIndex(input, targetWidth, options.width);
100
+ const nearestSpace = findNearestSpace(input, stringIndex, true);
101
+ const nearestSpaceWidth = indexToWidth(input, nearestSpace, options.width);
102
+ return ellipsis + slice(input, nearestSpaceWidth, width, { width: options.width }).trim();
78
103
  }
79
104
  return ellipsis + slice(input, width - limit + ellipsisWidth, width, {
80
105
  width: options.width
@@ -1,4 +1,4 @@
1
- import { l as IntervalArray } from './packem_shared/types-Daae0hGf.js';
1
+ import { l as IntervalArray } from './packem_shared/types-D2ydhHCv.js';
2
2
  export { L as LRUCache } from './packem_shared/lru-cache-B_t27XwI.js';
3
3
 
4
4
  declare const escapeRegExp: (char: string) => string;
package/dist/word-wrap.js CHANGED
@@ -1,9 +1,9 @@
1
- import { O as ESCAPES, P as ANSI_ESCAPE_LINK, Q as ANSI_SGR_TERMINATOR, S as ANSI_ESCAPE_BELL, T as ANSI_CSI, U as RE_ESCAPE_PATTERN, V as ANSI_RESET_CODES, W as END_CODE, X as RE_ZERO_WIDTH } from './packem_shared/constants-CTzC6Gjg.js';
1
+ import { O as ESCAPES, P as ANSI_ESCAPE_LINK, Q as ANSI_SGR_TERMINATOR, S as ANSI_ESCAPE_BELL, T as ANSI_CSI, U as RE_ESCAPE_PATTERN, V as ANSI_RESET_CODES, W as END_CODE, X as RE_ZERO_WIDTH } from './packem_shared/constants-CnXdp6qx.js';
2
2
  import { getStringWidth } from './get-string-width.js';
3
3
 
4
4
  class AnsiStateTracker {
5
- activeForeground = null;
6
- activeBackground = null;
5
+ activeForeground = void 0;
6
+ activeBackground = void 0;
7
7
  // Track other formatting (bold, italic, etc.)
8
8
  activeFormatting = [];
9
9
  /**
@@ -18,17 +18,17 @@ class AnsiStateTracker {
18
18
  const code = Number.parseInt(match[1], 10);
19
19
  switch (code) {
20
20
  case 0: {
21
- this.activeForeground = null;
22
- this.activeBackground = null;
21
+ this.activeForeground = void 0;
22
+ this.activeBackground = void 0;
23
23
  this.activeFormatting = [];
24
24
  break;
25
25
  }
26
26
  case 39: {
27
- this.activeForeground = null;
27
+ this.activeForeground = void 0;
28
28
  break;
29
29
  }
30
30
  case 49: {
31
- this.activeBackground = null;
31
+ this.activeBackground = void 0;
32
32
  break;
33
33
  }
34
34
  default: {
@@ -92,7 +92,7 @@ class AnsiStateTracker {
92
92
  "\x1B[9m": "\x1B[29m"
93
93
  // Strikethrough
94
94
  };
95
- [...this.activeFormatting].reverse().forEach((formatCode) => {
95
+ [...this.activeFormatting].toReversed().forEach((formatCode) => {
96
96
  const resetCode = formatResetMap[formatCode];
97
97
  if (resetCode) {
98
98
  closingEscapes.push(resetCode);
@@ -356,7 +356,8 @@ const wrapWithBreakAtWidth = (string, width, trim) => {
356
356
  let isInsideEscape = false;
357
357
  let isInsideLinkEscape = false;
358
358
  let escapeBuffer = "";
359
- for (let index = 0; index < string.length; index++) {
359
+ let index = 0;
360
+ while (index < string.length) {
360
361
  const char = string[index];
361
362
  if (ESCAPES.has(char)) {
362
363
  isInsideEscape = true;
@@ -364,6 +365,7 @@ const wrapWithBreakAtWidth = (string, width, trim) => {
364
365
  currentLine += char;
365
366
  const escapeInfo = checkEscapeSequence([...string], index);
366
367
  isInsideLinkEscape = escapeInfo.isInsideLinkEscape;
368
+ index += 1;
367
369
  continue;
368
370
  }
369
371
  if (isInsideEscape) {
@@ -377,12 +379,14 @@ const wrapWithBreakAtWidth = (string, width, trim) => {
377
379
  isInsideEscape = false;
378
380
  ansiTracker.processEscape(escapeBuffer);
379
381
  }
382
+ index += 1;
380
383
  continue;
381
384
  }
382
385
  const charWidth = getStringWidth(char);
383
386
  const isSpace = char === " ";
384
387
  if (charWidth === 0) {
385
388
  currentLine += char;
389
+ index += 1;
386
390
  continue;
387
391
  }
388
392
  if (currentWidth + charWidth > width) {
@@ -394,12 +398,6 @@ const wrapWithBreakAtWidth = (string, width, trim) => {
394
398
  if (isSpace && trim) {
395
399
  while (index < string.length && string[index] === " ") {
396
400
  index += 1;
397
- if (index >= string.length) {
398
- break;
399
- }
400
- }
401
- if (index < string.length) {
402
- index--;
403
401
  }
404
402
  continue;
405
403
  }
@@ -415,9 +413,10 @@ const wrapWithBreakAtWidth = (string, width, trim) => {
415
413
  while (index < string.length && string[index] === " ") {
416
414
  index += 1;
417
415
  }
418
- index--;
416
+ continue;
419
417
  }
420
418
  }
419
+ index += 1;
421
420
  }
422
421
  if (currentLine) {
423
422
  rows.push(currentLine + ansiTracker.getEndEscapesForAllActiveAttributes());
@@ -437,6 +436,7 @@ const wrapCharByChar = (string, width, trim) => {
437
436
  let currentWidth = 0;
438
437
  processAnsiString(inputToProcess, {
439
438
  getWidth: getStringWidth,
439
+ // eslint-disable-next-line sonarjs/cognitive-complexity,sonarjs/no-invariant-returns
440
440
  onSegment: (segment, stateTracker) => {
441
441
  if (segment.isEscapeSequence) {
442
442
  currentLine += segment.text;
@@ -550,8 +550,8 @@ const wrapAndBreakWords = (string, width, trim) => {
550
550
  }
551
551
  const brokenLines = wrapWithBreakAtWidth(token, width, trim);
552
552
  if (brokenLines.length > 0) {
553
- for (let index_ = 0; index_ < brokenLines.length - 1; index_++) {
554
- rows.push(brokenLines[index_]);
553
+ for (let brokenLineIndex = 0; brokenLineIndex < brokenLines.length - 1; brokenLineIndex += 1) {
554
+ rows.push(brokenLines[brokenLineIndex]);
555
555
  }
556
556
  currentLine = brokenLines[brokenLines.length - 1];
557
557
  currentWidth = getStringWidth(currentLine);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visulima/string",
3
- "version": "2.0.6",
3
+ "version": "3.0.0-alpha.2",
4
4
  "description": "Functions for manipulating strings.",
5
5
  "keywords": [
6
6
  "visulima",
@@ -57,7 +57,7 @@
57
57
  "repository": {
58
58
  "type": "git",
59
59
  "url": "git+https://github.com/visulima/visulima.git",
60
- "directory": "packages/string"
60
+ "directory": "packages/data-manipulation/string"
61
61
  },
62
62
  "funding": [
63
63
  {
@@ -228,7 +228,7 @@
228
228
  "CHANGELOG.md"
229
229
  ],
230
230
  "engines": {
231
- "node": ">=20.19 <=25.x"
231
+ "node": ">=22.13 <=25.x"
232
232
  },
233
233
  "publishConfig": {
234
234
  "access": "public",
@@ -1,19 +0,0 @@
1
- import { distance } from './closest-IHuSvnSn.js';
2
- export { closest } from './closest-IHuSvnSn.js';
3
-
4
- const closestN = (string_, array, n) => {
5
- const distances = new Array(n).fill(Infinity);
6
- const values = new Array(n).fill(null);
7
- for (let value of array) {
8
- let distribution = distance(string_, value);
9
- for (let index = 0; index < n; index++) {
10
- if (distribution < distances[index]) {
11
- [distribution, distances[index]] = [distances[index], distribution];
12
- [value, values[index]] = [values[index], value];
13
- }
14
- }
15
- }
16
- return values;
17
- };
18
-
19
- export { closestN, distance };