generated at
ScrapboxからGyazoのOCR結果を検索するUserScript
scrapboxの全文検索ページから、Gyazo OCRを検索するUserScript

仕組み
全文検索すると、ページの一番最後に Search With Gyazo ボタンが表示される
これを押すと、検索文字列が含まれる画像を探し、さらにその画像が埋め込まれているページを探す

originalとの違い
typescriptで書いている
deno-gyazoを使っている
画像が含まれているページをapi/pages/:projectname/search/queryで全文検索する
これでだいぶ高速化できているはず

やっぱりproject横断検索したいな~
でも予め決められた範囲のprojectを検索するのが限界だよな

このbuttonで作ったコードにコメントを加えたもの
↓コードを開くとTamperMonkeyが自動で認識してinstallしてくれる
script.user.js
// ==UserScript== // @name gyazo-search-proxy // @version 0.1.2 // @description Gyazoの検索機能をScrapboxから使えるようにする // @author takker // @namespace https://scrapbox.io // @match https://scrapbox.io/* // @grant GM_xmlhttpRequest // @connect gyazo.com // @license MIT // @copyright Copyright (c) 2022 takker // ==/UserScript== var r=(e,n)=>new Promise((o,t)=>{GM_xmlhttpRequest({method:c(n?.method),headers:u(n?.headers),url:typeof e=="string"?e:e.url,onload:({responseText:i,status:a,statusText:d})=>{let l=new Response(i,{status:a,statusText:d});o(l)},onerror:i=>t(i)})}),c=e=>{let n=e?.toUpperCase?.();switch(n){case"GET":case"HEAD":case"POST":case void 0:return n;default:return}},u=e=>{if(e===void 0)return e;if(Array.isArray(e))return Object.fromEntries(e);if(e instanceof Headers){let n={};return e.forEach((o,t)=>n[t]=o),n}return e};var s=async(e,n)=>{let o=`https://gyazo.com/api/internal/search_result?page=${n?.page??1}&per=${n?.per??40}&query=${encodeURIComponent(e)}`,t=await r(o);if(!t.ok)throw Error(`${t.status} ${t.statusText}`);let{captures:i}=await t.json();return i};unsafeWindow.searchGyazo=s;



使ったもの
$ deno check --remote -r=https://scrapbox.io https://scrapbox.io/api/code/takker/ScrapboxからGyazoのOCR結果を検索するUserScript/script.ts
script.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> import { searchForPages } from "../scrapbox-userscript-std/rest.ts"; import { toTitleLc, useStatusBar } from "../scrapbox-userscript-std/dom.ts"; import { pool } from "../async-lib/mod.ts"; import type { Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts"; import type { SearchGyazo } from "./gyazo.ts"; import { makeGyazoArea, makeSearchResult } from "./dom.ts"; declare const scrapbox: Scrapbox; declare global { interface Window { searchGyazo: SearchGyazo; } } export const setup = () => { launch(); scrapbox.addListener("page:changed", launch); }; const launch = () => { if (scrapbox.Layout !== "list") return; if (!/search\/page/.test(location.href)) return; const { button, list } = makeGyazoArea(); button.addEventListener("click", async () => { const { render, dispose } = useStatusBar(); try { const query = new URLSearchParams(location.search).get("q") ?? ""; render({ type: "spinner" }, { type: "text", text: `Searching Gyazo for "${query}"...`, }); const images = await window.searchGyazo(query); const project = scrapbox.Project.name; const titles = new Set<string>(); await Promise.all([...pool(3, images, async ({ url, image_id }) => { if (!image_id || !url) return; const result = await searchForPages(image_id, project); if (!result.ok) return; const { pages } = result.value; for (const { title, lines } of pages) { if (titles.has(toTitleLc(title))) continue; titles.add(toTitleLc(title)); render({ type: "spinner" }, { type: "text", text: `Searching Gyazo for "${query}"... Found ${titles.size} pages.`, }); list.append(makeSearchResult(project, title, url, lines)); } })]); render({ type: "check-circle" }, { type: "text", text: `Found ${titles.size} pages.`, }); } catch (e) { if ("name" in e && "message" in e) { render({ type: "exclamation-triangle" }, { type: "text", text: `${e.name} ${e.message}`, }); } else { render({ type: "exclamation-triangle" }, { type: "text", text: `Unexpected error occurs! (see developer console)`, }); } console.error(e); } finally { setTimeout(dispose, 1000); } }); };

dom.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> /** 検索結果表示欄を作る */ export const makeGyazoArea = (): { button: HTMLButtonElement; list: HTMLUListElement; } => { const area = document.createElement("div"); area.classList.add("project-search"); const count = document.createElement("div"); count.classList.add("project-search-count"); count.textContent = "Search with Gyazo"; area.append(count); const form = document.createElement("div"); form.classList.add("text-center"); const button = document.createElement("button"); button.id = "gyazo-button"; button.type = "submit"; button.classList.add( "project-search-button", "btn", "btn-auto-block", "btn-default", ); button.textContent = "Search with Gyazo"; form.append(button); area.append(form); const list = document.createElement("ul"); list.classList.add("list"); list.style.paddingBottom = "15px"; area.append(list); document.querySelector(".project-search")?.append?.(area); return { button, list }; }; /** 検索結果を作る */ export const makeSearchResult = ( project: string, title: string, imageURL: string, descriptions: string[], ): HTMLLIElement => { const item = document.createElement("li"); item.classList.add("page-list-item", "list-style-item"); const a = document.createElement("a"); a.href = `/${project}/${encodeURIComponent(title)}`; a.rel = "route"; a.style.display = "flex"; a.target = "_blank"; item.append(a); const imageArea = document.createElement("div"); imageArea.style.paddingRight = "20px"; const image = document.createElement("img"); image.loading = "lazy"; image.src = imageURL; image.style.width = "250px"; image.style.maxHeight = "400px"; imageArea.append(image); const summary = document.createElement("div"); summary.style.flex = "1"; const titleDiv = document.createElement("div"); titleDiv.classList.add("title-with-description"); titleDiv.textContent = title; summary.append(titleDiv); const description = document.createElement("div"); description.classList.add("description"); for (const line of descriptions) { const span = document.createElement("span"); span.textContent = line; description.append(span); } summary.append(description); a.append(imageArea, summary); return item; };

TamperMonkey側
user.js
import { searchGyazo } from "./gyazo.ts"; unsafeWindow.searchGyazo = searchGyazo;
gyazo.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> import "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/tampermonkey/index.d.ts"; import { fetch } from "./fetch.ts"; export interface Image { image_id: string; thumb_url: string; permalink_url: string | null; url: string | null; } export interface SearchGyazoOptions { page?: number; /** 最大検索件数 * * @default 40 */ per?: number; } export const searchGyazo = async ( query: string, options?: SearchGyazoOptions, ): Promise<Image[]> => { const url = `https://gyazo.com/api/internal/search_result?page=${ options?.page ?? 1 }&per=${options?.per ?? 40}&query=${encodeURIComponent(query)}`; const res = await fetch(url); if (!res.ok) throw Error(`${res.status} ${res.statusText}`); const { captures } = (await res.json()) as { captures: Image[] }; return captures; }; export type SearchGyazo = typeof searchGyazo;

fetch.ts
/// <reference no-default-lib="true" /> /// <reference lib="esnext" /> /// <reference lib="dom" /> import "https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/master/types/tampermonkey/index.d.ts"; import type { Fetch } from "../scrapbox-userscript-std/rest.ts"; export const fetch: Fetch = (input, init) => new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: toMethod(init?.method), headers: toHeaders(init?.headers), url: typeof input === "string" ? input : input.url, onload: ({ responseText, status, statusText }) => { const response = new Response(responseText, { status, statusText, }); resolve(response); }, onerror: (reason) => reject(reason), }); }); const toMethod = ( method: string | undefined, ): "GET" | "HEAD" | "POST" | undefined => { const value = method?.toUpperCase?.(); switch (value) { case "GET": case "HEAD": case "POST": case undefined: return value; default: return undefined; } }; const toHeaders = ( headers: HeadersInit | undefined, ): Tampermonkey.RequestHeaders | undefined => { if (headers === undefined) return headers; if (Array.isArray(headers)) { return Object.fromEntries(headers); } if (headers instanceof Headers) { const record: Record<string, string> = {}; headers.forEach((value, key) => record[key] = value); return record; } return headers; };

#2023-01-26 10:53:36
#2022-04-05 15:51:42
#2022-03-30 20:06:22