generated at
project補完テスト
scrapbox projectを補完するテスト
ASearchの挙動テストその2のUIを使って試してみる

2024-07-22
21:40:11 Levenshtein距離の計算を↓のように工夫した
これで問題なさそう
21:28:27 ようやくバグを潰せた
動作はいい感じ
最大Levenshtein距離は文字列全体の長さではなく、空白で区切ったときの単語の最大長にあわせるほうがよさそう
例えば foo bar baz は文字列長11なので3文字まで許容される
すると foo がどこにも含まれていない文字列もmatchしてしまう



main.ts
import { mount } from "./App.tsx"; mount();

$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/project補完テスト/App.tsx
App.tsx
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> /** @jsx h */ /** @jsxFrag Fragment */ import { Fragment, h, render } from "../preact/mod.tsx"; import { useCallback, useMemo, useEffect, useState, } from "../preact/hooks.ts"; import { Asearch, MatchResult } from "../deno-asearch/mod.ts"; import { getMaxDistance } from "./distance.ts"; import { makeSource } from "./makeSource.ts"; export const mount = () => { const app = document.createElement("div"); const shadowRoot = app.attachShadow({ mode: "open" }); document.body.append(app); remove = () => app.remove(); render(<App />, shadowRoot); }; let remove: () => void; const App = () => { const [pattern_, setPattern] = useState(""); const [data, setData] = useState<{ name: string, displayName: string }[]>([]); useEffect(() => { (async () => { for await (const candidates of makeSource()) { setData((prev) => [...prev, ...candidates]); } })(); }, []); const candidates = useMemo( () => { const pattern = pattern_.trim().replace(/^\//, "").replace(/\/$/, ""); if (pattern.length === 0) return []; const match = Asearch(`${pattern} `).match; const maxDistance = getMaxDistance[ Math.max(...pattern.split(/\s+/).map((word) => word.trim().length)) ]; return data.flatMap( ({ name, displayName }) => { const result1 = match(name, maxDistance); const result2 = match(displayName, maxDistance); if (!result1.found) { if (!result2.found) return []; return [{ candidate: displayName, distance: result2.distance }]; } if (!result2.found) return [{ candidate: name, distance: result1.distance }]; return result1.distance > result2.distance ? [{ candidate: displayName, distance: result2.distance }] : [{ candidate: name, distance: result1.distance }]; } ) // 1. 編集距離 2. 文字列超 3. 辞書順序 が小さい順に並び替える .sort((a, b) => { const diff = a.distance - b.distance; if (diff !== 0) return diff; const lenDiff = a.candidate.length - b.candidate.length; if (lenDiff !== 0) return lenDiff; return a.candidate.localeCompare(b.candidate); }); }, [pattern_, data], ); const handlePattern = useCallback( (e: h.JSX.TargetedEvent<HTMLInputElement>) => setPattern(e.currentTarget.value), [], ); return ( <> <style> {` :host { position: fixed; top: 60px; left: 50%; transform: translate(-50%, 0); padding: 5px; border: 1px solid lime; border-radius: 5px; font-size: 14px; background-color: var(--page-bg); color: var(--page-text-color); } input { min-width: 40%; } button { position: absolute; top: 0px; right: 0px; } `} </style> <button onClick={remove}>x</button> <p> <label> pattern: <input type="text" value={pattern_} onInput={handlePattern} /> </label> </p> <p> {candidates.length > 0 ? `Matched ${candidates.length} words` : "No matched"} <br /> <ul> {candidates.map(({ candidate }) => (<li key={candidate}>{candidate}</li>))} </ul> </p> </> ); };


distance.ts
export const getMaxDistance = [ 0, // 空文字のとき 0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, ];

データを作る
makeSource.ts
import { listProjects } from "../scrapbox-userscript-std/rest.ts"; export async function* makeSource(): AsyncGenerator<{ name: string, displayName: string }[]> { const watchList = JSON.parse(localStorage.getItem("projectsLastAccessed") ?? "{}"); const chunk = 50; const ids = [...Object.keys(watchList)]; const yielded = new Set<string>(); for (let i = 0; i <= Math.floor(ids.length / chunk); i++) { const res = await listProjects(ids.slice(i * chunk, (i + 1) * chunk)); if (!res.ok) continue; yield res.value.projects.flatMap((project) => { if (yielded.has(project.name)) return []; yielded.add(project.name); return [project]; }); } }


#2024-07-22 19:15:01