@weborigami/language 0.0.73 → 0.1.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.
@@ -1,4 +1,5 @@
1
1
  import { trailingSlash } from "@weborigami/async-tree";
2
+ import codeFragment from "../runtime/codeFragment.js";
2
3
  import * as ops from "../runtime/ops.js";
3
4
 
4
5
  // Parser helpers
@@ -8,15 +9,21 @@ import * as ops from "../runtime/ops.js";
8
9
  * location of the source code that produced it for debugging and error messages.
9
10
  */
10
11
  export function annotate(parseResult, location) {
11
- if (typeof parseResult === "object" && parseResult !== null) {
12
+ if (typeof parseResult === "object" && parseResult !== null && location) {
12
13
  parseResult.location = location;
14
+ parseResult.source = codeFragment(location);
13
15
  }
14
16
  return parseResult;
15
17
  }
16
18
 
17
- // The indicated code is being used to define a property named by the given key.
18
- // Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
19
- // infinite recursion.
19
+ /**
20
+ * The indicated code is being used to define a property named by the given key.
21
+ * Rewrite any [ops.scope, key] calls to be [ops.inherited, key] to avoid
22
+ * infinite recursion.
23
+ *
24
+ * @param {import("../../index.ts").Code} code
25
+ * @param {string} key
26
+ */
20
27
  function avoidRecursivePropertyCalls(code, key) {
21
28
  if (!(code instanceof Array)) {
22
29
  return code;
@@ -35,8 +42,7 @@ function avoidRecursivePropertyCalls(code, key) {
35
42
  // Process any nested code
36
43
  modified = code.map((value) => avoidRecursivePropertyCalls(value, key));
37
44
  }
38
- // @ts-ignore
39
- modified.location = code.location;
45
+ annotate(modified, code.location);
40
46
  return modified;
41
47
  }
42
48
 
@@ -133,7 +139,7 @@ export function makeFunctionCall(target, chain, location) {
133
139
  }
134
140
  }
135
141
 
136
- fnCall.location = { start, source, end };
142
+ annotate(fnCall, { start, source, end });
137
143
 
138
144
  value = fnCall;
139
145
  }
@@ -1,4 +1,4 @@
1
- import { handleExtension } from "./extensions.js";
1
+ import { handleExtension } from "./handlers.js";
2
2
 
3
3
  /**
4
4
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
@@ -1,7 +1,7 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { pathToFileURL } from "node:url";
4
- import { maybeOrigamiSourceCode } from "./formatError.js";
4
+ import { maybeOrigamiSourceCode } from "./errors.js";
5
5
 
6
6
  /**
7
7
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
@@ -7,8 +7,8 @@ export default function codeFragment(location) {
7
7
  : // Use entire source
8
8
  source.text;
9
9
 
10
- // Remove newlines and whitespace runs.
11
- fragment = fragment.replace(/(\n|\s\s+)+/g, "");
10
+ // Replace newlines and whitespace runs with a single space.
11
+ fragment = fragment.replace(/(\n|\s\s+)+/g, " ");
12
12
 
13
13
  // If longer than 80 characters, truncate with an ellipsis.
14
14
  if (fragment.length > 80) {
@@ -0,0 +1,104 @@
1
+ // Text we look for in an error stack to guess whether a given line represents a
2
+
3
+ import { scope as scopeFn, trailingSlash } from "@weborigami/async-tree";
4
+ import codeFragment from "./codeFragment.js";
5
+ import { typos } from "./typos.js";
6
+
7
+ // function in the Origami source code.
8
+ const origamiSourceSignals = [
9
+ "async-tree/src/",
10
+ "language/src/",
11
+ "origami/src/",
12
+ "at Scope.evaluate",
13
+ ];
14
+
15
+ export async function builtinReferenceError(tree, builtins, key) {
16
+ const messages = [
17
+ `"${key}" is being called as if it were a builtin function, but it's not.`,
18
+ ];
19
+ // See if the key is in scope (but not as a builtin)
20
+ const scope = scopeFn(tree);
21
+ const value = await scope.get(key);
22
+ if (value === undefined) {
23
+ const typos = await formatScopeTypos(builtins, key);
24
+ messages.push(typos);
25
+ } else {
26
+ messages.push(`Use "${key}/" instead.`);
27
+ }
28
+ const message = messages.join(" ");
29
+ return new ReferenceError(message);
30
+ }
31
+
32
+ /**
33
+ * Format an error for display in the console.
34
+ *
35
+ * @param {Error} error
36
+ */
37
+ export function formatError(error) {
38
+ let message;
39
+ if (error.stack) {
40
+ // Display the stack only until we reach the Origami source code.
41
+ message = "";
42
+ let lines = error.stack.split("\n");
43
+ for (let i = 0; i < lines.length; i++) {
44
+ const line = lines[i];
45
+ if (maybeOrigamiSourceCode(line)) {
46
+ break;
47
+ }
48
+ if (message) {
49
+ message += "\n";
50
+ }
51
+ message += lines[i];
52
+ }
53
+ } else {
54
+ message = error.toString();
55
+ }
56
+
57
+ // Add location
58
+ let location = /** @type {any} */ (error).location;
59
+ if (location) {
60
+ const fragment = codeFragment(location);
61
+ let { source, start } = location;
62
+
63
+ message += `\nevaluating: ${fragment}`;
64
+ if (typeof source === "object" && source.url) {
65
+ message += `\n at ${source.url.href}:${start.line}:${start.column}`;
66
+ } else if (source.text.includes("\n")) {
67
+ message += `\n at line ${start.line}, column ${start.column}`;
68
+ }
69
+ }
70
+ return message;
71
+ }
72
+
73
+ export async function formatScopeTypos(scope, key) {
74
+ const keys = await scopeTypos(scope, key);
75
+ // Don't match deprecated keys
76
+ const filtered = keys.filter((key) => !key.startsWith("@"));
77
+ if (filtered.length === 0) {
78
+ return "";
79
+ }
80
+ const quoted = filtered.map((key) => `"${key}"`);
81
+ const list = quoted.join(", ");
82
+ return `Maybe you meant ${list}?`;
83
+ }
84
+
85
+ export function maybeOrigamiSourceCode(text) {
86
+ return origamiSourceSignals.some((signal) => text.includes(signal));
87
+ }
88
+
89
+ export async function scopeReferenceError(scope, key) {
90
+ const messages = [
91
+ `"${key}" is not in scope.`,
92
+ await formatScopeTypos(scope, key),
93
+ ];
94
+ const message = messages.join(" ");
95
+ return new ReferenceError(message);
96
+ }
97
+
98
+ // Return all possible typos for `key` in scope
99
+ async function scopeTypos(scope, key) {
100
+ const scopeKeys = [...(await scope.keys())];
101
+ const normalizedScopeKeys = scopeKeys.map((key) => trailingSlash.remove(key));
102
+ const normalizedKey = trailingSlash.remove(key);
103
+ return typos(normalizedKey, normalizedScopeKeys);
104
+ }
@@ -68,10 +68,10 @@ export default async function evaluate(code) {
68
68
  if (!error.location) {
69
69
  // Attach the location of the code we tried to evaluate.
70
70
  error.location =
71
- error.position !== undefined
71
+ error.position !== undefined && code[error.position + 1]?.location
72
72
  ? // Use location of the argument with the given position (need to
73
73
  // offset by 1 to skip the function).
74
- code[error.position + 1].location
74
+ code[error.position + 1]?.location
75
75
  : // Use overall location.
76
76
  code.location;
77
77
  }
@@ -85,7 +85,7 @@ export default async function evaluate(code) {
85
85
  }
86
86
 
87
87
  // To aid debugging, add the code to the result.
88
- if (Object.isExtensible(result) /* && !isPlainObject(result) */) {
88
+ if (Object.isExtensible(result)) {
89
89
  try {
90
90
  if (code.location && !result[sourceSymbol]) {
91
91
  Object.defineProperty(result, sourceSymbol, {
@@ -1,5 +1,5 @@
1
- import { ObjectTree, symbols } from "@weborigami/async-tree";
2
- import { extname, handleExtension } from "./extensions.js";
1
+ import { extension, ObjectTree, symbols, Tree } from "@weborigami/async-tree";
2
+ import { handleExtension } from "./handlers.js";
3
3
  import { evaluate, ops } from "./internal.js";
4
4
 
5
5
  /**
@@ -22,6 +22,9 @@ import { evaluate, ops } from "./internal.js";
22
22
  export default async function expressionObject(entries, parent) {
23
23
  // Create the object and set its parent
24
24
  const object = {};
25
+ if (parent !== null && !Tree.isAsyncTree(parent)) {
26
+ throw new TypeError(`Parent must be an AsyncTree or null`);
27
+ }
25
28
  Object.defineProperty(object, symbols.parent, {
26
29
  configurable: true,
27
30
  enumerable: false,
@@ -37,8 +40,8 @@ export default async function expressionObject(entries, parent) {
37
40
  // array), we need to define a getter -- but if that code takes the form
38
41
  // [ops.getter, <primitive>], we can define a regular property.
39
42
  let defineProperty;
40
- const extension = extname(key);
41
- if (extension) {
43
+ const extname = extension.extname(key);
44
+ if (extname) {
42
45
  defineProperty = false;
43
46
  } else if (!(value instanceof Array)) {
44
47
  defineProperty = true;
@@ -76,7 +79,7 @@ export default async function expressionObject(entries, parent) {
76
79
  }
77
80
 
78
81
  let get;
79
- if (extension) {
82
+ if (extname) {
80
83
  // Key has extension, getter will invoke code then attach unpack method
81
84
  get = async () => {
82
85
  tree ??= new ObjectTree(object);
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  box,
3
+ extension,
3
4
  isPacked,
4
5
  isStringLike,
5
6
  isUnpackable,
@@ -13,27 +14,6 @@ import {
13
14
  // Track extensions handlers for a given containing tree.
14
15
  const handlersForContainer = new Map();
15
16
 
16
- /**
17
- * If the given path ends in an extension, return it. Otherwise, return the
18
- * empty string.
19
- *
20
- * This is meant as a basic replacement for the standard Node `path.extname`.
21
- * That standard function inaccurately returns an extension for a path that
22
- * includes a near-final extension but ends in a final slash, like `foo.txt/`.
23
- * Node thinks that path has a ".txt" extension, but for our purposes it
24
- * doesn't.
25
- *
26
- * @param {string} path
27
- */
28
- export function extname(path) {
29
- // We want at least one character before the dot, then a dot, then a non-empty
30
- // sequence of characters after the dot that aren't slahes or dots.
31
- const extnameRegex = /[^/](?<ext>\.[^/\.]+)$/;
32
- const match = String(path).match(extnameRegex);
33
- const extension = match?.groups?.ext.toLowerCase() ?? "";
34
- return extension;
35
- }
36
-
37
17
  /**
38
18
  * Find an extension handler for a file in the given container.
39
19
  *
@@ -95,9 +75,11 @@ export async function handleExtension(parent, value, key) {
95
75
  }
96
76
 
97
77
  // Special case: `.ori.<ext>` extensions are Origami documents.
98
- const extension = key.match(/\.ori\.\S+$/) ? ".oridocument" : extname(key);
99
- if (extension) {
100
- const handler = await getExtensionHandler(parent, extension);
78
+ const extname = key.match(/\.ori\.\S+$/)
79
+ ? ".oridocument"
80
+ : extension.extname(key);
81
+ if (extname) {
82
+ const handler = await getExtensionHandler(parent, extname);
101
83
  if (handler) {
102
84
  if (hasSlash && handler.unpack) {
103
85
  // Key like `data.json/` ends in slash -- unpack immediately
@@ -6,6 +6,7 @@
6
6
  //
7
7
  // About this pattern: https://medium.com/visual-development/how-to-fix-nasty-circular-dependency-issues-once-and-for-all-in-javascript-typescript-a04c987cf0de
8
8
  //
9
+ // Note: to avoid having VS Code auto-sort the imports, keep lines between them.
9
10
 
10
11
  export * as ops from "./ops.js";
11
12
 
@@ -1,30 +1,25 @@
1
1
  /**
2
2
  * @typedef {import("@weborigami/types").AsyncTree} AsyncTree
3
3
  * @typedef {import("@weborigami/async-tree").PlainObject} PlainObject
4
+ * @typedef {import("@weborigami/async-tree").Treelike} Treelike
4
5
  */
5
6
 
6
7
  import {
7
- ExplorableSiteTree,
8
8
  ObjectTree,
9
- SiteTree,
10
9
  Tree,
11
10
  isUnpackable,
12
- pathFromKeys,
13
11
  scope as scopeFn,
14
- trailingSlash,
15
12
  concat as treeConcat,
16
13
  } from "@weborigami/async-tree";
14
+ import os from "node:os";
15
+ import { builtinReferenceError, scopeReferenceError } from "./errors.js";
17
16
  import expressionObject from "./expressionObject.js";
18
- import { handleExtension } from "./extensions.js";
19
- import HandleExtensionsTransform from "./HandleExtensionsTransform.js";
20
17
  import { evaluate } from "./internal.js";
21
18
  import mergeTrees from "./mergeTrees.js";
22
19
  import OrigamiFiles from "./OrigamiFiles.js";
20
+ import { codeSymbol } from "./symbols.js";
23
21
  import taggedTemplate from "./taggedTemplate.js";
24
22
 
25
- // For memoizing lambda functions
26
- const lambdaFnMap = new Map();
27
-
28
23
  function addOpLabel(op, label) {
29
24
  Object.defineProperty(op, "toString", {
30
25
  value: () => label,
@@ -43,6 +38,29 @@ export async function array(...items) {
43
38
  }
44
39
  addOpLabel(array, "«ops.array»");
45
40
 
41
+ /**
42
+ * Like ops.scope, but only searches for a builtin at the top of the scope
43
+ * chain.
44
+ *
45
+ * @this {AsyncTree|null}
46
+ */
47
+ export async function builtin(key) {
48
+ if (!this) {
49
+ throw new Error("Tried to get the scope of a null or undefined tree.");
50
+ }
51
+ let current = this;
52
+ while (current.parent) {
53
+ current = current.parent;
54
+ }
55
+
56
+ const value = await current.get(key);
57
+ if (value === undefined) {
58
+ throw await builtinReferenceError(this, current, key);
59
+ }
60
+
61
+ return value;
62
+ }
63
+
46
64
  /**
47
65
  * Look up the given key in the scope for the current tree the first time
48
66
  * the key is requested, holding on to the value for future requests.
@@ -73,106 +91,6 @@ export async function concat(...args) {
73
91
  }
74
92
  addOpLabel(concat, "«ops.concat»");
75
93
 
76
- /**
77
- * Find the indicated constructor in scope, then return a function which invokes
78
- * it with `new`.
79
- *
80
- * @this {AsyncTree}
81
- * @param {...any} keys
82
- */
83
- export async function constructor(...keys) {
84
- const tree = this;
85
- const scope = scopeFn(tree);
86
- let constructor = await Tree.traverseOrThrow(scope, ...keys);
87
- if (isUnpackable(constructor)) {
88
- constructor = await constructor.unpack();
89
- }
90
- // Origami may pass `undefined` as the first argument to the constructor. We
91
- // don't pass that along, because constructors like `Date` don't like it.
92
- return (...args) =>
93
- args.length === 1 && args[0] === undefined
94
- ? new constructor()
95
- : new constructor(...args);
96
- }
97
- addOpLabel(constructor, "«ops.constructor»");
98
-
99
- /**
100
- * Given a protocol, a host, and a list of keys, construct an href.
101
- *
102
- * @param {string} protocol
103
- * @param {string} host
104
- * @param {string[]} keys
105
- */
106
- function constructHref(protocol, host, ...keys) {
107
- const path = pathFromKeys(keys);
108
- let href = [host, path].join("/");
109
- if (!href.startsWith(protocol)) {
110
- if (!href.startsWith("//")) {
111
- href = `//${href}`;
112
- }
113
- href = `${protocol}${href}`;
114
- }
115
- return href;
116
- }
117
-
118
- /**
119
- * Given a protocol, a host, and a list of keys, construct an href.
120
- *
121
- * @param {string} protocol
122
- * @param {import("../../index.ts").Constructor<AsyncTree>} treeClass
123
- * @param {AsyncTree|null} parent
124
- * @param {string} host
125
- * @param {string[]} keys
126
- */
127
- async function constructSiteTree(protocol, treeClass, parent, host, ...keys) {
128
- // If the last key doesn't end in a slash, remove it for now.
129
- let lastKey;
130
- if (keys.length > 0 && keys.at(-1) && !trailingSlash.has(keys.at(-1))) {
131
- lastKey = keys.pop();
132
- }
133
-
134
- const href = constructHref(protocol, host, ...keys);
135
- let result = new (HandleExtensionsTransform(treeClass))(href);
136
- result.parent = parent;
137
-
138
- return lastKey ? result.get(lastKey) : result;
139
- }
140
-
141
- /**
142
- * A site tree with JSON Keys via HTTPS.
143
- *
144
- * @this {AsyncTree|null}
145
- * @param {string} host
146
- * @param {...string} keys
147
- */
148
- export function explorableSite(host, ...keys) {
149
- return constructSiteTree("https:", ExplorableSiteTree, this, host, ...keys);
150
- }
151
- addOpLabel(explorableSite, "«ops.explorableSite»");
152
-
153
- /**
154
- * Fetch the resource at the given href.
155
- *
156
- * @this {AsyncTree|null}
157
- * @param {string} href
158
- */
159
- async function fetchResponse(href) {
160
- const response = await fetch(href);
161
- if (!response.ok) {
162
- return undefined;
163
- }
164
- let buffer = await response.arrayBuffer();
165
-
166
- // Attach any loader defined for the file type.
167
- const url = new URL(href);
168
- const filename = url.pathname.split("/").pop();
169
- if (this && filename) {
170
- buffer = await handleExtension(this, buffer, filename);
171
- }
172
-
173
- return buffer;
174
- }
175
-
176
94
  /**
177
95
  * This op is only used during parsing. It signals to ops.object that the
178
96
  * "arguments" of the expression should be used to define a property getter.
@@ -180,46 +98,26 @@ async function fetchResponse(href) {
180
98
  export const getter = new String("«ops.getter»");
181
99
 
182
100
  /**
183
- * Construct a files tree for the filesystem root.
101
+ * Files tree for the filesystem root.
184
102
  *
185
103
  * @this {AsyncTree|null}
186
104
  */
187
105
  export async function filesRoot() {
188
- let root = new OrigamiFiles("/");
189
-
190
- // The root itself needs a parent so that expressions evaluated within it
191
- // (e.g., Origami expressions loaded from .ori files) will have access to
192
- // things like the built-in functions.
193
- root.parent = this;
194
-
195
- return root;
106
+ let tree = new OrigamiFiles("/");
107
+ tree.parent = root(this);
108
+ return tree;
196
109
  }
197
110
 
198
111
  /**
199
- * Retrieve a web resource via HTTP.
112
+ * Files tree for the user's home directory.
200
113
  *
201
114
  * @this {AsyncTree|null}
202
- * @param {string} host
203
- * @param {...string} keys
204
115
  */
205
- export async function http(host, ...keys) {
206
- const href = constructHref("http:", host, ...keys);
207
- return fetchResponse.call(this, href);
116
+ export async function homeTree() {
117
+ const tree = new OrigamiFiles(os.homedir());
118
+ tree.parent = root(this);
119
+ return tree;
208
120
  }
209
- addOpLabel(http, "«ops.http»");
210
-
211
- /**
212
- * Retrieve a web resource via HTTPS.
213
- *
214
- * @this {AsyncTree|null}
215
- * @param {string} host
216
- * @param {...string} keys
217
- */
218
- export function https(host, ...keys) {
219
- const href = constructHref("https:", host, ...keys);
220
- return fetchResponse.call(this, href);
221
- }
222
- addOpLabel(https, "«ops.https»");
223
121
 
224
122
  /**
225
123
  * Search the parent's scope -- i.e., exclude the current tree -- for the given
@@ -247,19 +145,21 @@ addOpLabel(inherited, "«ops.inherited»");
247
145
  */
248
146
 
249
147
  export function lambda(parameters, code) {
250
- if (lambdaFnMap.has(code)) {
251
- return lambdaFnMap.get(code);
252
- }
148
+ const context = this;
253
149
 
254
- /** @this {AsyncTree|null} */
150
+ /** @this {Treelike|null} */
255
151
  async function invoke(...args) {
256
152
  // Add arguments to scope.
257
153
  const ambients = {};
258
154
  for (const parameter of parameters) {
259
155
  ambients[parameter] = args.shift();
260
156
  }
157
+ Object.defineProperty(ambients, codeSymbol, {
158
+ value: code,
159
+ enumerable: false,
160
+ });
261
161
  const ambientTree = new ObjectTree(ambients);
262
- ambientTree.parent = this;
162
+ ambientTree.parent = context;
263
163
 
264
164
  let result = await evaluate.call(ambientTree, code);
265
165
 
@@ -286,7 +186,6 @@ export function lambda(parameters, code) {
286
186
  });
287
187
 
288
188
  invoke.code = code;
289
- lambdaFnMap.set(code, invoke);
290
189
  return invoke;
291
190
  }
292
191
  addOpLabel(lambda, "«ops.lambda");
@@ -323,6 +222,16 @@ export async function object(...entries) {
323
222
  }
324
223
  addOpLabel(object, "«ops.object»");
325
224
 
225
+ // Return the root of the given tree. For an Origami tree, this gives us
226
+ // a way of acessing the builtins.
227
+ function root(tree) {
228
+ let current = tree;
229
+ while (current.parent) {
230
+ current = current.parent;
231
+ }
232
+ return current;
233
+ }
234
+
326
235
  /**
327
236
  * Look up the given key in the scope for the current tree.
328
237
  *
@@ -333,7 +242,11 @@ export async function scope(key) {
333
242
  throw new Error("Tried to get the scope of a null or undefined tree.");
334
243
  }
335
244
  const scope = scopeFn(this);
336
- return scope.get(key);
245
+ const value = await scope.get(key);
246
+ if (value === undefined) {
247
+ throw await scopeReferenceError(scope, key);
248
+ }
249
+ return value;
337
250
  }
338
251
  addOpLabel(scope, "«ops.scope»");
339
252
 
@@ -361,30 +274,6 @@ addOpLabel(template, "«ops.template»");
361
274
  */
362
275
  export const traverse = Tree.traverseOrThrow;
363
276
 
364
- /**
365
- * A website tree via HTTP.
366
- *
367
- * @this {AsyncTree|null}
368
- * @param {string} host
369
- * @param {...string} keys
370
- */
371
- export function treeHttp(host, ...keys) {
372
- return constructSiteTree("http:", SiteTree, this, host, ...keys);
373
- }
374
- addOpLabel(treeHttp, "«ops.treeHttp»");
375
-
376
- /**
377
- * A website tree via HTTPS.
378
- *
379
- * @this {AsyncTree|null}
380
- * @param {string} host
381
- * @param {...string} keys
382
- */
383
- export function treeHttps(host, ...keys) {
384
- return constructSiteTree("https:", SiteTree, this, host, ...keys);
385
- }
386
- addOpLabel(treeHttps, "«ops.treeHttps»");
387
-
388
277
  /**
389
278
  * If the value is packed but has an unpack method, call it and return that as
390
279
  * the result; otherwise, return the value as is.
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Returns true if the two strings have a Damerau-Levenshtein distance of 1.
3
+ * This will be true if the strings differ by a single insertion, deletion,
4
+ * substitution, or transposition.
5
+ *
6
+ * @param {string} s1
7
+ * @param {string} s2
8
+ */
9
+ export function isTypo(s1, s2) {
10
+ const length1 = s1.length;
11
+ const length2 = s2.length;
12
+
13
+ // If the strings are identical, distance is 0
14
+ if (s1 === s2) {
15
+ return false;
16
+ }
17
+
18
+ // If length difference is more than 1, distance can't be 1
19
+ if (Math.abs(length1 - length2) > 1) {
20
+ return false;
21
+ }
22
+
23
+ if (length1 === length2) {
24
+ // Check for one substitution
25
+ let differences = 0;
26
+ for (let i = 0; i < length1; i++) {
27
+ if (s1[i] !== s2[i]) {
28
+ differences++;
29
+ if (differences > 1) {
30
+ break;
31
+ }
32
+ }
33
+ }
34
+ if (differences === 1) {
35
+ return true;
36
+ }
37
+
38
+ // Check for one transposition
39
+ for (let i = 0; i < length1 - 1; i++) {
40
+ if (s1[i] !== s2[i]) {
41
+ // Check if swapping s1[i] and s1[i+1] matches s2
42
+ if (s1[i] === s2[i + 1] && s1[i + 1] === s2[i]) {
43
+ return s1.slice(i + 2) === s2.slice(i + 2);
44
+ } else {
45
+ return false;
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ // Check for one insertion/deletion
52
+ const longer = length1 > length2 ? s1 : s2;
53
+ const shorter = length1 > length2 ? s2 : s1;
54
+ for (let i = 0; i < shorter.length; i++) {
55
+ if (shorter[i] !== longer[i]) {
56
+ // If we skip this character, do the rest match?
57
+ return shorter.slice(i) === longer.slice(i + 1);
58
+ }
59
+ }
60
+ return shorter === longer.slice(0, shorter.length);
61
+ }
62
+
63
+ /**
64
+ * Return any strings that could be a typo of s
65
+ *
66
+ * @param {string} s
67
+ * @param {string[]} strings
68
+ */
69
+ export function typos(s, strings) {
70
+ return strings.filter((str) => isTypo(s, str));
71
+ }