diff --git a/package.json b/package.json index 7c4f3fc05..ff58ef9cd 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "@types/luxon": "^2.3.2", "@types/node": "^16.7.13", "@types/parsimmon": "^1.10.6", + "@types/path-browserify": "^1.0.3", "builtin-modules": "3.3.0", "esbuild": "^0.16.11", "esbuild-plugin-inline-worker": "https://github.com/mitschabaude/esbuild-plugin-inline-worker", @@ -52,6 +53,7 @@ "localforage": "1.10.0", "luxon": "^2.4.0", "parsimmon": "^1.18.0", + "path-browserify": "^1.0.1", "preact": "^10.17.1", "react-select": "^5.8.0", "sorted-btree": "^1.8.1", diff --git a/src/api/local-api.tsx b/src/api/local-api.tsx index 0edf2bfaa..791096dea 100644 --- a/src/api/local-api.tsx +++ b/src/api/local-api.tsx @@ -92,7 +92,7 @@ export class DatacoreLocalApi { * ``` */ public async require(path: string | Link): Promise { - const result = await this.scriptCache.load(path, { dc: this }); + const result = await this.scriptCache.load(path, this); return result.orElseThrow(); } diff --git a/src/api/script-cache.ts b/src/api/script-cache.ts index 535d89148..0f1fd2c29 100644 --- a/src/api/script-cache.ts +++ b/src/api/script-cache.ts @@ -3,10 +3,16 @@ import { Datastore } from "index/datastore"; import { Result } from "./result"; import { MarkdownCodeblock, MarkdownSection } from "index/types/markdown"; import { Deferred, deferred } from "utils/deferred"; -import { ScriptLanguage, asyncEvalInContext, transpile } from "utils/javascript"; +import { + ScriptDefinition, + ScriptLanguage, + asyncEvalInContext, + defaultScriptLoadingContext, + transpile, +} from "utils/javascript"; import { lineRange } from "utils/normalizers"; -import { TFile } from "obsidian"; -import { Fragment, h } from "preact"; +import { normalizePath, TFile } from "obsidian"; +import { DatacoreLocalApi } from "./local-api"; /** A script that is currently being loaded. */ export interface LoadingScript { @@ -56,9 +62,21 @@ export class ScriptCache { public constructor(private store: Datastore) {} /** Load the given script at the given path, recursively loading any subscripts as well. */ - public async load(path: string | Link, context: Record): Promise> { + public async load(path: string | Link, api: DatacoreLocalApi): Promise> { + // First, attempt to resolve the script against the script roots so we cache a canonical script path as the key. + var linkToLoad = undefined; + const roots = ["", ...api.core.settings.scriptRoots]; + for (var i = 0; i < roots.length; i++) { + linkToLoad = this.store.tryNormalizeLink(path, normalizePath(roots[i])); + if (linkToLoad) { + break; + } + } + + const resolvedPath = linkToLoad ?? path; + // Always check the cache first. - const key = this.pathkey(path); + const key = this.pathkey(resolvedPath); const currentScript = this.scripts.get(key); if (currentScript) { if (currentScript.type === "loaded") return Result.success(currentScript.object); @@ -68,8 +86,8 @@ export class ScriptCache { // we are in a `require()` loop. Either way, we'll error out for now since we can't handle // either case currently. return Result.failure( - `Failed to import script "${path.toString()}", as it is in the middle of being loaded. Do you have - a circular dependency in your require() calls? The currently loaded or loading scripts are: + `Failed to import script "${resolvedPath.toString()}", as it is in the middle of being loaded. Do you have + a circular dependency in your require() calls? The currently loaded or loading scripts are: ${Array.from(this.scripts.values()) .map((sc) => "\t" + sc.path) .join("\n")}` @@ -80,7 +98,7 @@ export class ScriptCache { const deferral = deferred>(); this.scripts.set(key, { type: "loading", promise: deferral, path: key }); - const result = await this.loadUncached(path, context); + const result = await this.loadUncached(resolvedPath, api); deferral.resolve(result); if (result.successful) { @@ -93,25 +111,30 @@ export class ScriptCache { } /** Load a script, directly bypassing the cache. */ - private async loadUncached(path: string | Link, context: Record): Promise> { - const maybeSource = await this.resolveSource(path); + private async loadUncached(scriptPath: string | Link, api: DatacoreLocalApi): Promise> { + var maybeSource = await this.resolveSource(scriptPath); if (!maybeSource.successful) return maybeSource; // Transpile to vanilla javascript first... - const { code, language } = maybeSource.value; + const scriptDefinition = maybeSource.value; let basic; try { - basic = transpile(code, language); + basic = transpile(scriptDefinition); } catch (error) { - return Result.failure(`Failed to import ${path.toString()} while transpiling from ${language}: ${error}`); + return Result.failure( + `Failed to import ${scriptPath.toString()} while transpiling from ${ + scriptDefinition.scriptLanguage + }: ${error}` + ); } // Then finally execute the script to 'load' it. - const finalContext = Object.assign({ h: h, Fragment: Fragment }, context); + const scriptContext = defaultScriptLoadingContext(api); try { - return Result.success(await asyncEvalInContext(basic, finalContext)); + const loadRet = (await asyncEvalInContext(basic, scriptContext)) ?? scriptContext.exports; + return Result.success(loadRet); } catch (error) { - return Result.failure(`Failed to execute script '${path.toString()}': ${error}`); + return Result.failure(`Failed to execute script '${scriptPath.toString()}': ${error}`); } } @@ -122,10 +145,8 @@ export class ScriptCache { } /** Attempts to resolve the source to load given a path or link to a markdown section. */ - private async resolveSource( - path: string | Link - ): Promise> { - const object = this.store.resolveLink(path); + private async resolveSource(path: string | Link, sourcePath?: string): Promise> { + const object = this.store.resolveLink(path, sourcePath); if (!object) return Result.failure("Could not find a script at the given path: " + path.toString()); const tfile = this.store.vault.getFileByPath(object.$file!); @@ -137,7 +158,7 @@ export class ScriptCache { try { const code = await this.store.vault.cachedRead(tfile); - return Result.success({ code, language }); + return Result.success({ scriptFile: tfile, scriptLanguage: language, scriptSource: code }); } catch (error) { return Result.failure("Failed to load javascript/typescript source file: " + error); } @@ -158,7 +179,11 @@ export class ScriptCache { ScriptCache.SCRIPT_LANGUAGES[ maybeBlock.$languages.find((lang) => lang.toLocaleLowerCase() in ScriptCache.SCRIPT_LANGUAGES)! ]; - return (await this.readCodeblock(tfile, maybeBlock)).map((code) => ({ code, language })); + return (await this.readCodeblock(tfile, maybeBlock)).map((code) => ({ + scriptFile: tfile, + scriptSource: code, + scriptLanguage: language, + })); } else if (object instanceof MarkdownCodeblock) { const maybeLanguage = object.$languages.find( (lang) => lang.toLocaleLowerCase() in ScriptCache.SCRIPT_LANGUAGES @@ -167,7 +192,11 @@ export class ScriptCache { return Result.failure(`The codeblock referenced by '${path}' is not a JS/TS codeblock.`); const language = ScriptCache.SCRIPT_LANGUAGES[maybeLanguage]; - return (await this.readCodeblock(tfile, object)).map((code) => ({ code, language })); + return (await this.readCodeblock(tfile, object)).map((code) => ({ + scriptFile: tfile, + scriptSource: code, + scriptLanguage: language, + })); } return Result.failure(`Cannot import '${path.toString()}: not a JS/TS file or codeblock reference.`); diff --git a/src/index/datastore.ts b/src/index/datastore.ts index 1b627a6b7..0628012f9 100644 --- a/src/index/datastore.ts +++ b/src/index/datastore.ts @@ -4,7 +4,7 @@ import { FolderIndex } from "index/storage/folder"; import { InvertedIndex } from "index/storage/inverted"; import { IndexPrimitive, IndexQuery, IndexSource } from "index/types/index-query"; import { Indexable, LINKABLE_TYPE, LINKBEARING_TYPE, TAGGABLE_TYPE } from "index/types/indexable"; -import { MetadataCache, Vault } from "obsidian"; +import { MetadataCache, normalizePath, Vault } from "obsidian"; import { MarkdownPage } from "./types/markdown"; import { extractSubtags, normalizeHeaderForLink } from "utils/normalizers"; import FlatQueue from "flatqueue"; @@ -14,6 +14,7 @@ import { IndexResolver, execute, optimizeQuery } from "index/storage/query-execu import { Result } from "api/result"; import { Evaluator } from "expression/evaluator"; import { Settings } from "settings"; +import path from "path-browserify"; /** Central, index storage for datacore values. */ export class Datastore { @@ -266,15 +267,44 @@ export class Datastore { this.revision++; } - /** Find the corresponding object for a given link. */ - public resolveLink(rawLink: string | Link, sourcePath?: string): Indexable | undefined { + public tryNormalizeLink(rawLink: string | Link, sourcePath?: string): Link | undefined { let link = typeof rawLink === "string" ? Link.parseInner(rawLink) : rawLink; - if (sourcePath) { const linkdest = this.metadataCache.getFirstLinkpathDest(link.path, sourcePath); if (linkdest) link = link.withPath(linkdest.path); } + if (this.objects.has(link.path)) { + return link; + } + + const normalizedSourcePath = normalizePath(sourcePath ?? "/"); + const normalizedModuleParentDir = normalizePath(path.join(normalizedSourcePath, path.dirname(link.path))); + const resolvedModuleFile = this.folder + .getExact(normalizedModuleParentDir, (childPath) => { + const moduleBasename = path.basename(link.path); + const childFileBasename = path.basename(childPath); + return childFileBasename === moduleBasename || childFileBasename.startsWith(`${moduleBasename}.`); + }) + .values() + .next()?.value; + if (resolvedModuleFile) { + return link.withPath(resolvedModuleFile); + } + + return undefined; + } + + public normalizeLink(rawLink: string | Link, sourcePath?: string): Link { + return ( + this.tryNormalizeLink(rawLink, sourcePath) ?? + (typeof rawLink === "string" ? Link.parseInner(rawLink) : rawLink) + ); + } + + /** Find the corresponding object for a given link. */ + public resolveLink(rawLink: string | Link, sourcePath?: string): Indexable | undefined { + const link = this.normalizeLink(rawLink, sourcePath); const file = this.objects.get(link.path); if (!file) return undefined; diff --git a/src/main.ts b/src/main.ts index 274448bd7..ff2b46513 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,7 +1,7 @@ import { DatacoreApi } from "api/api"; import { Datacore } from "index/datacore"; import { DateTime } from "luxon"; -import { App, Plugin, PluginSettingTab, Setting } from "obsidian"; +import { App, Plugin, PluginSettingTab, SearchComponent, Setting } from "obsidian"; import { DEFAULT_SETTINGS, Settings } from "settings"; /** Reactive data engine for your Obsidian.md vault. */ @@ -96,6 +96,29 @@ class GeneralSettingsTab extends PluginSettingTab { super(app, plugin); } + private async handleNewScriptRoot(component?: SearchComponent) { + if (!component) { + return; + } + + const searchValue = component.getValue(); + if (!searchValue || searchValue.length === 0) { + return; + } + + if (this.plugin.settings.scriptRoots.has(searchValue)) { + return; + } + + const dirStat = await this.app.vault.adapter.stat(searchValue); + if (!(dirStat?.type === "folder")) { + return; + } + + await this.plugin.addScriptRootsToSettings([searchValue]); + this.display(); + } + public display(): void { this.containerEl.empty(); @@ -252,5 +275,74 @@ class GeneralSettingsTab extends PluginSettingTab { await this.plugin.updateSettings({ maxRecursiveRenderDepth: parsed }); }); }); + + this.containerEl.createEl("h2", { text: "Scripts" }); + + const importRootsDesc = new DocumentFragment(); + importRootsDesc.createDiv().innerHTML = ` +

+Provide folders in the vault to be used when resolving module and/or script file names, +in addition to the vault root. These values are used with with +require(...)/await dc.require(...)/import ... when the path +does not start with some kind of indicator for resolving the root +(such as ./ or /). +

+`; + var searchBar: SearchComponent | undefined = undefined; + new Setting(this.containerEl).setName("Additional Script/Module Roots").setDesc(importRootsDesc); + new Setting(this.containerEl).addSearch(searchComponent => { + searchBar = searchComponent; + const searcher = new FuzzyFolderSearchSuggest(this.app, searchComponent.inputEl); + searcher.limit = 10; + searcher.onSelect(async (val, evt) => { + evt.preventDefault(); + evt.stopImmediatePropagation(); + evt.stopPropagation(); + searcher.setValue(val); + searcher.close(); + }); + + searchComponent.setPlaceholder("New Script Root Folder..."); + searchComponent.onChange((val) => { + if (val.length > 0) { + searcher.open(); + } + }); + + searchComponent.inputEl.addEventListener("keydown", (evt) => { + if (evt.key === "Enter") { + this.handleNewScriptRoot(searchComponent); + } + }); + searchComponent.inputEl.addClass("datacore-settings-full-width-search-input"); + }) + .addButton((buttonComponent) => { + buttonComponent + .setIcon("plus") + .setTooltip("Add Folder To Script Roots") + .onClick(async () => { + await this.handleNewScriptRoot(searchBar); + }); + }); + + this.plugin.settings.scriptRoots.forEach((root) => { + const folderItem = new Setting(this.containerEl); + const folderItemFragment = new DocumentFragment(); + const folderItemDiv = folderItemFragment.createDiv(); + folderItemDiv.addClasses(["datacore-settings-script-root", "setting-item-info"]); + setIcon(folderItemDiv, "folder"); + folderItemDiv.createEl("h2", { text: root }); + folderItem.infoEl.replaceWith(folderItemFragment); + folderItem.addButton((buttonComponent) => { + buttonComponent + .setIcon("cross") + .setTooltip("Remove Folder from Script Roots") + .onClick(async () => { + await this.plugin.removeScriptRoots([root]); + this.display(); + }); + }); + }); + } } diff --git a/src/settings.css b/src/settings.css new file mode 100644 index 000000000..7fd9d9605 --- /dev/null +++ b/src/settings.css @@ -0,0 +1,29 @@ +.setting-item-control:has(.datacore-settings-full-width-search-input), +.search-input-container:has(.datacore-settings-full-width-search-input), +.datacore-settings-full-width-search-input { + width: 100%; +} + +.setting-item:has(.datacore-settings-full-width-search-input) { + border-bottom: 1px solid var(--background-modifier-border); + margin-bottom: 15px; +} + +.setting-item:has(.datacore-settings-full-width-search-input) .setting-item-info { + margin-inline-end: 0; + display: none; +} + +.setting-item:has(.datacore-settings-script-root) { + border: none; + padding: 0; + margin-bottom: 10px; +} + +.datacore-settings-script-root { + display: inline-flex !important; + align-items: center; +} +.datacore-settings-script-root h2 { + margin: 8px; +} diff --git a/src/settings.ts b/src/settings.ts index 082d4170d..5e2468ac1 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -18,6 +18,9 @@ export interface Settings { /** If set, views will scroll to the top of the view on page changes. */ scrollOnPageChange: boolean; + /** Vault folders to be used when resolving required/imported scripts in addition to the vault root. */ + scriptRoots: string[]; + /** * Maximum depth that objects will be rendered to (i.e., how many levels of subproperties * will be rendered by default). This avoids infinite recursion due to self referential objects @@ -49,6 +52,8 @@ export const DEFAULT_SETTINGS: Readonly = Object.freeze({ defaultPageSize: 50, scrollOnPageChange: false, + scriptRoots: [], + maxRecursiveRenderDepth: 5, defaultDateFormat: "MMMM dd, yyyy", diff --git a/src/typings/obsidian-ex.d.ts b/src/typings/obsidian-ex.d.ts index 80ce8d328..111380e60 100644 --- a/src/typings/obsidian-ex.d.ts +++ b/src/typings/obsidian-ex.d.ts @@ -4,6 +4,10 @@ import "obsidian"; /** @hidden */ declare module "obsidian" { + interface MetadataCache { + isUserIgnored(path: string): boolean; + } + interface FileManager { linkUpdaters: { canvas: { diff --git a/src/ui/javascript.tsx b/src/ui/javascript.tsx index 2202273e9..774ecc61d 100644 --- a/src/ui/javascript.tsx +++ b/src/ui/javascript.tsx @@ -1,9 +1,9 @@ import { ErrorMessage, SimpleErrorBoundary, CURRENT_FILE_CONTEXT, DatacoreContextProvider } from "ui/markdown"; import { App, MarkdownRenderChild } from "obsidian"; import { DatacoreLocalApi } from "api/local-api"; -import { h, render, Fragment, VNode } from "preact"; +import { render, VNode } from "preact"; import { unmountComponentAtNode } from "preact/compat"; -import { ScriptLanguage, asyncEvalInContext, transpile } from "utils/javascript"; +import { ScriptLanguage, asyncEvalInContext, defaultScriptLoadingContext, transpile } from "utils/javascript"; import { LoadingBoundary, ScriptContainer } from "./loading-boundary"; import { Datacore } from "index/datacore"; @@ -29,13 +29,16 @@ export class DatacoreJSRenderer extends MarkdownRenderChild { // Attempt to parse and evaluate the script to produce either a renderable JSX object or a function. try { - const primitiveScript = transpile(this.script, this.language); + const primitiveScript = transpile({ + scriptSource: this.script, + scriptLanguage: this.language, + scriptFile: this.api.app.vault.getFileByPath(this.path)!, + }); + const scriptContext = defaultScriptLoadingContext(this.api); const renderer = async () => { - return await asyncEvalInContext(primitiveScript, { - dc: this.api, - h: h, - Fragment: Fragment, - }); + const loadedScript = + (await asyncEvalInContext(primitiveScript, scriptContext)) ?? scriptContext.exports; + return loadedScript; }; render( diff --git a/src/ui/loading-boundary.tsx b/src/ui/loading-boundary.tsx index 353d7b290..79f0681f1 100644 --- a/src/ui/loading-boundary.tsx +++ b/src/ui/loading-boundary.tsx @@ -7,6 +7,8 @@ import { ErrorMessage, Lit } from "./markdown"; import "./errors.css"; +type Renderable = Literal | VNode | Function; + /** Simple view which shows datacore's current loading progress when it is still indexing on startup. */ function LoadingProgress({ datacore }: { datacore: Datacore }) { useIndexUpdates(datacore, { debounce: 250 }); @@ -54,7 +56,7 @@ export function ScriptContainer({ executor, sourcePath, }: { - executor: () => Promise; + executor: () => Promise Renderable }>; sourcePath: string; }) { const [element, setElement] = useState(undefined); @@ -65,7 +67,12 @@ export function ScriptContainer({ setError(undefined); executor() - .then((result) => setElement(makeRenderableElement(result, sourcePath))) + .then((result) => { + if (result && result.hasOwnProperty("default")) { + return setElement(makeRenderableElement((result as any).default, sourcePath)); + } + return setElement(makeRenderableElement(result, sourcePath)); + }) .catch((error) => setError(error)); }, [executor]); diff --git a/src/utils/javascript.ts b/src/utils/javascript.ts index e94f137a6..cd8966dc9 100644 --- a/src/utils/javascript.ts +++ b/src/utils/javascript.ts @@ -1,25 +1,59 @@ //! Utilities for running javascript. +import { DatacoreLocalApi } from "api/local-api"; +import { normalizePath, TFile } from "obsidian"; import { transform } from "sucrase"; +import path from "path-browserify"; + export type ScriptLanguage = "js" | "ts" | "jsx" | "tsx"; +export interface ScriptDefinition { + /** The file that the script source has been loaded from */ + scriptFile: TFile; + /** The script source code */ + scriptSource: string; + /** The script language */ + scriptLanguage: ScriptLanguage; +} + +export function defaultScriptLoadingContext(api: DatacoreLocalApi): Record { + return { + dc: api, + h: api.preact.h, + Fragment: api.preact.Fragment, + exports: {}, + }; +} + +export function newScriptLoadingContextWith(api: DatacoreLocalApi, other: Record): Record { + return { ...defaultScriptLoadingContext(api), ...other }; +} + /** Converts a raw script in the given language to plain javascript. */ -export function transpile(script: string, language: ScriptLanguage): string { - switch (language) { +export function transpile(script: ScriptDefinition): ScriptDefinition { + const transpiled = { ...script }; + + switch (script.scriptLanguage) { case "js": - return script; + transpiled.scriptSource = transform(script.scriptSource, { transforms: ["imports"] }).code; case "jsx": - return transform(script, { transforms: ["jsx"], jsxPragma: "h", jsxFragmentPragma: "Fragment" }).code; + transpiled.scriptSource = transform(script.scriptSource, { + transforms: ["jsx", "imports"], + jsxPragma: "h", + jsxFragmentPragma: "Fragment", + }).code; case "ts": - return transform(script, { transforms: ["typescript"] }).code; + transpiled.scriptSource = transform(script.scriptSource, { transforms: ["typescript", "imports"] }).code; case "tsx": - return transform(script, { - transforms: ["typescript", "jsx"], + transpiled.scriptSource = transform(script.scriptSource, { + transforms: ["typescript", "jsx", "imports"], jsxPragma: "h", jsxFragmentPragma: "Fragment", }).code; } + + return transpiled; } /** @@ -29,17 +63,38 @@ export function evalInContext(script: string, variables: Record): a const pairs = Object.entries(variables); const keys = pairs.map(([key, _]) => key); const values = pairs.map(([_, value]) => value); - return new Function(...keys, script)(...values); } +function resolveRelativeRequires(script: string, scriptFile: TFile): string { + const requireMatches = script.matchAll(/=\s*require\(([^\)]+)\)/gm); + for (const match of requireMatches) { + const requireTargetRaw = match[1]; + const requireTarget = requireTargetRaw.replace(/'/gm, "").replace(/"/gm, "").replace(/^\//, ""); + const regexEscapedRequire = requireTargetRaw.replace(/[.*+?^=!:${}()|\[\]\/\\]/g, "\\$&"); + const regExp = new RegExp(`=\\s*require\\(${regexEscapedRequire}\\)`, "gm"); + + var resolvedRequireTarget = requireTarget; + if (/^[\.]*\//gm.test(requireTarget)) { + resolvedRequireTarget = normalizePath(path.join(scriptFile.parent!.path, requireTarget)); + } + script = script.replace(regExp, `= await dc.require("${resolvedRequireTarget}")`); + } + return script; +} + /** * Evaluate a script possibly asynchronously, if the script contains `async/await` blocks. */ -export async function asyncEvalInContext(script: string, variables: Record): Promise { - if (script.includes("await")) { - return evalInContext("return (async () => { " + script + " })()", variables) as Promise; +export async function asyncEvalInContext(script: ScriptDefinition, variables: Record): Promise { + var { scriptSource, scriptFile } = script; + if (/=\s*require\(/gm.test(scriptSource)) { + scriptSource = resolveRelativeRequires(scriptSource, scriptFile); + } + + if (scriptSource.includes("await")) { + return evalInContext("return (async () => { " + scriptSource + " })()", variables) as Promise; } else { - return Promise.resolve(evalInContext(script, variables)); + return Promise.resolve(evalInContext(scriptSource, variables)); } } diff --git a/src/utils/settings/fuzzy-folder-finder.ts b/src/utils/settings/fuzzy-folder-finder.ts new file mode 100644 index 000000000..9302f410b --- /dev/null +++ b/src/utils/settings/fuzzy-folder-finder.ts @@ -0,0 +1,44 @@ +import { AbstractInputSuggest, App, prepareFuzzySearch, SearchResult } from "obsidian"; + +export class FuzzyFolderSearchSuggest extends AbstractInputSuggest { + constructor( + app: App, + inputEl: HTMLInputElement, + private threshold: number = -5, + private respectUserIgnored: boolean = false + ) { + super(app, inputEl); + } + + protected getSuggestions(query: string): string[] | Promise { + const searchFn = prepareFuzzySearch(query); + const accumulator: Record = {}; + const matchedFolders = this.app.vault.getAllFolders(false).reduce((accum, tfile) => { + const isIgnoredFile = this.respectUserIgnored && this.app.metadataCache.isUserIgnored(tfile.path); + if (isIgnoredFile) { + return accum; + } + + const searchResult = searchFn(tfile.path); + const noResults = !searchResult?.score; + const belowThreshold = searchResult?.score && searchResult.score < this.threshold; + + if (noResults || belowThreshold) { + return accum; + } + + if (!accum.hasOwnProperty(tfile.path) || accum[tfile.path].score < searchResult.score) { + accum[tfile.path] = searchResult; + } + return accum; + }, accumulator); + + return Object.entries(matchedFolders) + .sort((a, b) => b[1].score - a[1].score) + .map((result) => result[0]); + } + + renderSuggestion(value: string, el: HTMLElement): void { + el.setText(value); + } +} diff --git a/yarn.lock b/yarn.lock index 556e62d6c..86d27f701 100644 --- a/yarn.lock +++ b/yarn.lock @@ -985,6 +985,11 @@ resolved "https://registry.yarnpkg.com/@types/parsimmon/-/parsimmon-1.10.9.tgz#14e60db223c1d213fea0e15985d480b5cfe1789a" integrity sha512-O2M2x1w+m7gWLen8i5DOy6tWRnbRcsW6Pke3j3HAsJUrPb4g0MgjksIUm2aqUtCYxy7Qjr3CzjjwQBzhiGn46A== +"@types/path-browserify@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@types/path-browserify/-/path-browserify-1.0.3.tgz#25de712d4def94b3901f033c30d3d3bd16eba8d3" + integrity sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw== + "@types/prettier@^2.1.5": version "2.7.3" resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.3.tgz#3e51a17e291d01d17d3fc61422015a933af7a08f" @@ -2748,6 +2753,11 @@ parsimmon@^1.18.0: resolved "https://registry.yarnpkg.com/parsimmon/-/parsimmon-1.18.1.tgz#d8dd9c28745647d02fc6566f217690897eed7709" integrity sha512-u7p959wLfGAhJpSDJVYXoyMCXWYwHia78HhRBWqk7AIbxdmlrfdp5wX0l3xv/iTSH5HvhN9K7o26hwwpgS5Nmw== +path-browserify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd" + integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g== + path-exists@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3"