diff --git a/public/registry/index.json b/public/registry/index.json index 64bce76..e315cfb 100644 --- a/public/registry/index.json +++ b/public/registry/index.json @@ -1,24 +1,24 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "plugins": { "count": 43, - "updatedAt": "2026-10-28T19:35:34.231Z" + "updatedAt": "2026-10-28T23:08:30.161Z" }, "themes": { "count": 77, - "updatedAt": "2026-10-28T19:35:34.231Z" + "updatedAt": "2026-10-28T23:08:30.161Z" }, "widgets": { "count": 70, - "updatedAt": "2026-10-28T19:35:34.231Z" + "updatedAt": "2026-10-28T23:08:30.161Z" }, "skills": { "count": 44, - "updatedAt": "2026-10-28T19:35:34.231Z" + "updatedAt": "2026-10-28T23:08:30.161Z" }, "queries": { "count": 35, - "updatedAt": "2026-10-28T19:35:34.231Z" + "updatedAt": "2026-10-28T23:08:30.161Z" } } diff --git a/public/registry/plugins.json b/public/registry/plugins.json index 80f9dd6..a70ffbc 100644 --- a/public/registry/plugins.json +++ b/public/registry/plugins.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "plugins": [ { "id": "ai-actions", @@ -84,7 +84,7 @@ "apiUrl": "https://store.notedeck.io/registry/plugins/etherscan-autolink/api.json", "sha512": "2232153b654aafcd0d63e304d9cfeeed0b2021e6e0395e3bdb7089a5cc045547b4ddb36371b08b580fa48f94a141e47e2d7b810cc3434fe0c344ddf6b010ab1c", "createdAt": "2026-10-29T04:34:22+09:00", - "updatedAt": "2026-10-29T04:34:22+09:00", + "updatedAt": "2026-10-29T04:35:41+09:00", "authorUrl": "https://github.com/hitalin", "license": "MIT", "locales": { @@ -1917,7 +1917,7 @@ { "id": "tech-autolink", "name": "技術系オートリンク", - "version": "1.0.0", + "version": "1.1.0", "author": "hitalin", "description": "CVE / JVN / JVNVU / JVNTA / JVNDB / DLsite / ニコニコ動画 / GitHub コミットハッシュを投稿時に自動でリンクにするプラグイン。misskey-tempura の自動リンク化を、リモートにも届く Markdown リンクとして再現したもの", "category": "posting", @@ -1932,9 +1932,9 @@ ], "sourceUrl": "https://store.notedeck.io/registry/plugins/tech-autolink/plugin.is", "apiUrl": "https://store.notedeck.io/registry/plugins/tech-autolink/api.json", - "sha512": "4eaf43dbd5b2c4a7f0ed7dc39ba33459f0796e0da846ff044500a22a007c5ebfd20e8296dd65ebcfdc549ecef75bc6c12af3e0425f76c1b9413f2b539d0507f8", + "sha512": "e5e2d778c1b30c7a34bd4de6cc3a34f88b839887794f569f3989e04723bfa1b981c3c50a275e14d0538bde393f88dc9344f4d5f3a44fe1670ab5ab60439c56ec", "createdAt": "2026-07-31T15:01:56+09:00", - "updatedAt": "2026-09-25T18:20:03+09:00", + "updatedAt": "2026-10-29T07:57:04+09:00", "authorUrl": "https://github.com/hitalin", "license": "MIT", "locales": { diff --git a/public/registry/plugins/tech-autolink/api.json b/public/registry/plugins/tech-autolink/api.json index 93da7de..d76ccbe 100644 --- a/public/registry/plugins/tech-autolink/api.json +++ b/public/registry/plugins/tech-autolink/api.json @@ -1 +1 @@ -{"type":"plugin","data":"/// @ 1.2.1\n\n// 技術系オートリンク — CVE / JVN / DLsite / ニコニコ / コミットハッシュを自動リンク化\n//\n// misskey-tempura (taiyme 由来の tms/rj-number) が表示時に行っている\n// 自動リンク化を、投稿時のテキスト変換として再現したもの。\n// 表示時変換と違い Markdown リンクとして本文に残るため、\n// このプラグインを入れていない閲覧者やリモートのサーバーでもリンクになる。\n//\n// 誤爆を避けるため、以下は変換しない:\n// - コード (` / ```) の中\n// - URL の中、Markdown リンク ([ラベル](URL)) の中、ハッシュタグ\n// - 前後に英数字が隣接しているもの (XCVE-2024-1234 / CVE-2024-1234x など)\n\n### {\n\tname: \"技術系オートリンク\"\n\tversion: \"1.0.0\"\n\tauthor: \"@hitalin\"\n\tdescription: \"CVE / JVN / JVNVU / JVNTA / JVNDB / DLsite / ニコニコ動画 / GitHub コミットハッシュを投稿時に自動でリンクにします\"\n\tconfig: {\n\t\tsecurity: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"セキュリティ情報 (CVE / JVN / JVNVU / JVNTA / JVNDB)\"\n\t\t\tdefault: true\n\t\t}\n\t\tdlsite: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"DLsite (RJ / RE / VJ / VE / BJ / BE / RG など)\"\n\t\t\tdefault: true\n\t\t}\n\t\tnicovideo: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"ニコニコ動画 (sm / nm / so)\"\n\t\t\tdefault: true\n\t\t}\n\t\tgithubCommit: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"GitHub コミットハッシュ (40 桁)\"\n\t\t\tdefault: true\n\t\t}\n\t\tautoConvert: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"投稿時に自動で変換する\"\n\t\t\tdescription: \"オフの場合は投稿フォームのプラグインメニューから手動で変換します\"\n\t\t\tdefault: false\n\t\t}\n\t}\n}\n\nlet DIGITS = \"0123456789\"\nlet HEXCHARS = \"0123456789abcdefABCDEF\"\nlet WORDCHARS = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\n@isWordChar(c) {\n\t// 空文字に対して incl は true を返すため明示的に除く\n\tc != \"\" && WORDCHARS.incl(c)\n}\n\n@isBreak(c) {\n\tc == \" \" || c == \" \" || c == Str:lf\n}\n\n@sub(chars, from, to) {\n\tchars.slice(from, to).join(\"\")\n}\n\n// chars[start] から allowed に含まれる文字を読み進める\n@readRun(chars, start, allowed) {\n\tvar i = start\n\tloop {\n\t\tif (i >= chars.len) break\n\t\tif (!allowed.incl(chars[i])) break\n\t\ti += 1\n\t}\n\t{\n\t\tvalue: sub(chars, start, i)\n\t\tnext: i\n\t}\n}\n\n// chars の i 文字目から word が (大文字小文字を無視して) 続くか\n@startsWith(chars, i, word) {\n\tlet w = word.split()\n\tif (i + w.len > chars.len) return false\n\tvar ok = true\n\tfor (let k, w.len) {\n\t\tif (chars[i + k].lower() != w[k].lower()) ok = false\n\t}\n\tok\n}\n\n// --- 各記法のマッチャー: 成立すれば { label, url, next }、しなければ null ---\n\n// CVE-2024-1234\n@matchCve(chars, i) {\n\tif (!startsWith(chars, i, \"CVE-\")) return null\n\tlet year = readRun(chars, i + 4, DIGITS)\n\tif (year.value.len != 4) return null\n\tif (year.next >= chars.len || chars[year.next] != \"-\") return null\n\tlet seq = readRun(chars, year.next + 1, DIGITS)\n\tif (seq.value.len < 4) return null\n\tlet label = sub(chars, i, seq.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://www.cve.org/CVERecord?id={label.upper()}`\n\t\tnext: seq.next\n\t}\n}\n\n// JVNDB-2024-000001\n@matchJvndb(chars, i) {\n\tif (!startsWith(chars, i, \"JVNDB-\")) return null\n\tlet year = readRun(chars, i + 6, DIGITS)\n\tif (year.value.len != 4) return null\n\tif (year.next >= chars.len || chars[year.next] != \"-\") return null\n\tlet seq = readRun(chars, year.next + 1, DIGITS)\n\tif (seq.value.len != 6) return null\n\tlet label = sub(chars, i, seq.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://jvndb.jvn.jp/en/contents/{year.value}/{label.upper()}.html`\n\t\tnext: seq.next\n\t}\n}\n\n// JVN#12345678 / JVNVU#12345678 / JVNTA#12345678\n@matchJvnFamily(chars, i, prefix, path) {\n\tif (!startsWith(chars, i, prefix)) return null\n\tlet body = readRun(chars, i + prefix.len, `#{DIGITS}`)\n\tif (body.value.len < 8) return null\n\tlet label = sub(chars, i, body.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://jvn.jp/{path}/{label.replace(\"#\", \"\").upper()}/index.html`\n\t\tnext: body.next\n\t}\n}\n\n// RJ123456 (作品) / RG12345 (サークル)\n@matchDlsite(chars, i) {\n\tif (i + 1 >= chars.len) return null\n\tif (!\"RVB\".incl(chars[i])) return null\n\tlet kind = chars[i + 1]\n\tif (\"JE\".incl(kind)) {\n\t\tlet num = readRun(chars, i + 2, DIGITS)\n\t\tif (num.value.len < 6) return null\n\t\tlet label = sub(chars, i, num.next)\n\t\treturn {\n\t\t\tlabel: label\n\t\t\turl: `https://www.dlsite.com/home/work/=/product_id/{label}.html`\n\t\t\tnext: num.next\n\t\t}\n\t}\n\tif (kind == \"G\") {\n\t\tlet num = readRun(chars, i + 2, DIGITS)\n\t\tif (num.value.len < 5) return null\n\t\tlet label = sub(chars, i, num.next)\n\t\treturn {\n\t\t\tlabel: label\n\t\t\turl: `https://www.dlsite.com/home/circle/profile/=/maker_id/{label}.html`\n\t\t\tnext: num.next\n\t\t}\n\t}\n\tnull\n}\n\n// sm9 / nm12345 / so12345\n@matchNico(chars, i) {\n\tif (i + 1 >= chars.len) return null\n\tlet head = `{chars[i]}{chars[i + 1]}`\n\tif (head != \"sm\" && head != \"nm\" && head != \"so\") return null\n\tlet num = readRun(chars, i + 2, DIGITS)\n\tif (num.value.len < 1) return null\n\tlet label = sub(chars, i, num.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://www.nicovideo.jp/watch/{label}`\n\t\tnext: num.next\n\t}\n}\n\n// 40 桁ちょうどの 16 進 (SHA-1)。64 桁の SHA-256 などは長さで弾かれる\n@matchCommitHash(chars, i) {\n\tlet hex = readRun(chars, i, HEXCHARS)\n\tif (hex.value.len != 40) return null\n\t{\n\t\tlabel: hex.value\n\t\turl: `https://github.com/search?q={hex.value}&type=commits`\n\t\tnext: hex.next\n\t}\n}\n\n// 前方一致が長いものから試す\n@matchAny(chars, i, opts) {\n\tvar hit = null\n\tif (opts.security) {\n\t\tif (hit == null) hit = matchJvndb(chars, i)\n\t\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVNVU\", \"vu\")\n\t\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVNTA\", \"ta\")\n\t\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVN\", \"jp\")\n\t\tif (hit == null) hit = matchCve(chars, i)\n\t}\n\tif (opts.dlsite && hit == null) hit = matchDlsite(chars, i)\n\tif (opts.nicovideo && hit == null) hit = matchNico(chars, i)\n\tif (opts.githubCommit && hit == null) hit = matchCommitHash(chars, i)\n\thit\n}\n\n// --- 変換対象外の範囲を読み飛ばすための判定 ---\n\n// [ラベル](URL) の終端。成立しなければ null\n@readMdLink(chars, i) {\n\tvar j = i + 1\n\tloop {\n\t\tif (j >= chars.len) return null\n\t\tlet c = chars[j]\n\t\tif (c == \"]\") break\n\t\tif (c == Str:lf) return null\n\t\tj += 1\n\t}\n\tif (j + 1 >= chars.len || chars[j + 1] != \"(\") return null\n\tvar k = j + 2\n\tloop {\n\t\tif (k >= chars.len) return null\n\t\tlet c = chars[k]\n\t\tif (c == \")\") break\n\t\tif (isBreak(c)) return null\n\t\tk += 1\n\t}\n\tk + 1\n}\n\n// 空白までを 1 塊として読み飛ばす終端 (URL / ハッシュタグ用)\n@readUntilBreak(chars, i) {\n\tvar j = i\n\tloop {\n\t\tif (j >= chars.len) break\n\t\tif (isBreak(chars[j])) break\n\t\tj += 1\n\t}\n\tj\n}\n\n@convertLinks(text, opts) {\n\tlet chars = text.split()\n\tvar buf = \"\"\n\tvar count = 0\n\tvar i = 0\n\t// ` の数でコード内かどうかを判定する (``` も 1 文字ずつ反転して辻褄が合う)\n\tvar inCode = false\n\tloop {\n\t\tif (i >= chars.len) break\n\t\tlet ch = chars[i]\n\n\t\tif (ch == \"`\") {\n\t\t\tinCode = !inCode\n\t\t\tbuf = `{buf}{ch}`\n\t\t\ti += 1\n\t\t} elif (inCode) {\n\t\t\tbuf = `{buf}{ch}`\n\t\t\ti += 1\n\t\t} elif (ch == \"[\" && readMdLink(chars, i) != null) {\n\t\t\tlet end = readMdLink(chars, i)\n\t\t\tbuf = `{buf}{sub(chars, i, end)}`\n\t\t\ti = end\n\t\t} elif (startsWith(chars, i, \"https://\") || startsWith(chars, i, \"http://\")) {\n\t\t\tlet end = readUntilBreak(chars, i)\n\t\t\tbuf = `{buf}{sub(chars, i, end)}`\n\t\t\ti = end\n\t\t} elif (ch == \"#\" && (i == 0 || isBreak(chars[i - 1]))) {\n\t\t\tlet end = readUntilBreak(chars, i)\n\t\t\tbuf = `{buf}{sub(chars, i, end)}`\n\t\t\ti = end\n\t\t} elif (i > 0 && isWordChar(chars[i - 1])) {\n\t\t\t// 語の途中からは拾わない\n\t\t\tbuf = `{buf}{ch}`\n\t\t\ti += 1\n\t\t} else {\n\t\t\tlet hit = matchAny(chars, i, opts)\n\t\t\t// 記法の直後に英数字が続くものは誤検出とみなす\n\t\t\tlet tail = if (hit == null || hit.next >= chars.len) \"\" else chars[hit.next]\n\t\t\tif (hit == null || isWordChar(tail)) {\n\t\t\t\tbuf = `{buf}{ch}`\n\t\t\t\ti += 1\n\t\t\t} else {\n\t\t\t\tbuf = `{buf}[{hit.label}]({hit.url})`\n\t\t\t\tcount += 1\n\t\t\t\ti = hit.next\n\t\t\t}\n\t\t}\n\t}\n\t{\n\t\ttext: buf\n\t\tcount: count\n\t}\n}\n\n@currentOpts() {\n\t{\n\t\tsecurity: Plugin:config.security\n\t\tdlsite: Plugin:config.dlsite\n\t\tnicovideo: Plugin:config.nicovideo\n\t\tgithubCommit: Plugin:config.githubCommit\n\t}\n}\n\nPlugin:register_post_form_action(\"リンクに変換\", @(form, update) {\n\tlet text = if (form.text == null) \"\" else form.text\n\tlet result = convertLinks(text, currentOpts())\n\tif (result.count == 0) {\n\t\tMk:dialog(\"技術系オートリンク\", \"リンクにできる記法が見つかりません\", \"info\")\n\t\treturn null\n\t}\n\tupdate(\"text\", result.text)\n\tMk:toast(`{result.count} 件をリンクにしました`, \"success\")\n})\n\nPlugin:register_note_post_interruptor(@(note) {\n\tif (Plugin:config.autoConvert && note.text != null) {\n\t\tnote.text = convertLinks(note.text, currentOpts()).text\n\t}\n\tnote\n})\n"} +{"type":"plugin","data":"/// @ 1.2.1\n\n// 技術系オートリンク — CVE / JVN / DLsite / ニコニコ / コミットハッシュを自動リンク化\n//\n// misskey-tempura (taiyme 由来の tms/rj-number) が表示時に行っている\n// 自動リンク化を、投稿時のテキスト変換として再現したもの。\n// 表示時変換と違い Markdown リンクとして本文に残るため、\n// このプラグインを入れていない閲覧者やリモートのサーバーでもリンクになる。\n//\n// 誤爆を避けるため、以下は変換しない:\n// - コード (` / ```) の中\n// - URL の中、Markdown リンク ([ラベル](URL)) の中、ハッシュタグ\n// - 前後に英数字が隣接しているもの (XCVE-2024-1234 / CVE-2024-1234x など)\n//\n// 本文を 1 文字ずつ舐めると 1000 字前後で maxStep (100k) を超えるので、\n// 各記法の先頭文字列 (CVE- / JVN / RJ / sm …) の出現位置だけを str.index_of で拾い、\n// その前後だけを検査する。コミットハッシュだけは接頭辞が無いので 16 進の連なりを数える。\n\n### {\n\tname: \"技術系オートリンク\"\n\tversion: \"1.1.0\"\n\tauthor: \"@hitalin\"\n\tdescription: \"CVE / JVN / JVNVU / JVNTA / JVNDB / DLsite / ニコニコ動画 / GitHub コミットハッシュを投稿時に自動でリンクにします\"\n\tconfig: {\n\t\tsecurity: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"セキュリティ情報 (CVE / JVN / JVNVU / JVNTA / JVNDB)\"\n\t\t\tdefault: true\n\t\t}\n\t\tdlsite: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"DLsite (RJ / RE / VJ / VE / BJ / BE / RG など)\"\n\t\t\tdefault: true\n\t\t}\n\t\tnicovideo: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"ニコニコ動画 (sm / nm / so)\"\n\t\t\tdefault: true\n\t\t}\n\t\tgithubCommit: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"GitHub コミットハッシュ (40 桁)\"\n\t\t\tdefault: true\n\t\t}\n\t\tautoConvert: {\n\t\t\ttype: \"boolean\"\n\t\t\tlabel: \"投稿時に自動で変換する\"\n\t\t\tdescription: \"オフの場合は投稿フォームのプラグインメニューから手動で変換します\"\n\t\t\tdefault: false\n\t\t}\n\t}\n}\n\nlet DIGITS = \"0123456789\"\nlet HEXCHARS = \"0123456789abcdefABCDEF\"\nlet WORDCHARS = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n\n@isWordChar(c) {\n\t// 空文字に対して incl は true を返すため明示的に除く\n\tc != \"\" && WORDCHARS.incl(c)\n}\n\n@isBreak(c) {\n\tc == \" \" || c == \" \" || c == Str:lf\n}\n\n@sub(chars, from, to) {\n\tchars.slice(from, to).join(\"\")\n}\n\n// chars[start] から allowed に含まれる文字を読み進める\n@readRun(chars, start, allowed) {\n\tvar i = start\n\tloop {\n\t\tif (i >= chars.len) break\n\t\tif (!allowed.incl(chars[i])) break\n\t\ti += 1\n\t}\n\t{\n\t\tvalue: sub(chars, start, i)\n\t\tnext: i\n\t}\n}\n\n// chars の i 文字目から word が (大文字小文字を無視して) 続くか\n@startsWith(chars, i, word) {\n\tlet n = word.split().len\n\tif (i + n > chars.len) return false\n\tsub(chars, i, i + n).lower() == word.lower()\n}\n\n// --- 各記法のマッチャー: 成立すれば { label, url, next }、しなければ null ---\n\n// CVE-2024-1234\n@matchCve(chars, i) {\n\tif (!startsWith(chars, i, \"CVE-\")) return null\n\tlet year = readRun(chars, i + 4, DIGITS)\n\tif (year.value.len != 4) return null\n\tif (year.next >= chars.len || chars[year.next] != \"-\") return null\n\tlet seq = readRun(chars, year.next + 1, DIGITS)\n\tif (seq.value.len < 4) return null\n\tlet label = sub(chars, i, seq.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://www.cve.org/CVERecord?id={label.upper()}`\n\t\tnext: seq.next\n\t}\n}\n\n// JVNDB-2024-000001\n@matchJvndb(chars, i) {\n\tif (!startsWith(chars, i, \"JVNDB-\")) return null\n\tlet year = readRun(chars, i + 6, DIGITS)\n\tif (year.value.len != 4) return null\n\tif (year.next >= chars.len || chars[year.next] != \"-\") return null\n\tlet seq = readRun(chars, year.next + 1, DIGITS)\n\tif (seq.value.len != 6) return null\n\tlet label = sub(chars, i, seq.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://jvndb.jvn.jp/en/contents/{year.value}/{label.upper()}.html`\n\t\tnext: seq.next\n\t}\n}\n\n// JVN#12345678 / JVNVU#12345678 / JVNTA#12345678\n@matchJvnFamily(chars, i, prefix, path) {\n\tif (!startsWith(chars, i, prefix)) return null\n\tlet body = readRun(chars, i + prefix.len, `#{DIGITS}`)\n\tif (body.value.len < 8) return null\n\tlet label = sub(chars, i, body.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://jvn.jp/{path}/{label.replace(\"#\", \"\").upper()}/index.html`\n\t\tnext: body.next\n\t}\n}\n\n// RJ123456 (作品) / RG12345 (サークル)\n@matchDlsite(chars, i) {\n\tif (i + 1 >= chars.len) return null\n\tif (!\"RVB\".incl(chars[i])) return null\n\tlet kind = chars[i + 1]\n\tif (\"JE\".incl(kind)) {\n\t\tlet num = readRun(chars, i + 2, DIGITS)\n\t\tif (num.value.len < 6) return null\n\t\tlet label = sub(chars, i, num.next)\n\t\treturn {\n\t\t\tlabel: label\n\t\t\turl: `https://www.dlsite.com/home/work/=/product_id/{label}.html`\n\t\t\tnext: num.next\n\t\t}\n\t}\n\tif (kind == \"G\") {\n\t\tlet num = readRun(chars, i + 2, DIGITS)\n\t\tif (num.value.len < 5) return null\n\t\tlet label = sub(chars, i, num.next)\n\t\treturn {\n\t\t\tlabel: label\n\t\t\turl: `https://www.dlsite.com/home/circle/profile/=/maker_id/{label}.html`\n\t\t\tnext: num.next\n\t\t}\n\t}\n\tnull\n}\n\n// sm9 / nm12345 / so12345\n@matchNico(chars, i) {\n\tif (i + 1 >= chars.len) return null\n\tlet head = `{chars[i]}{chars[i + 1]}`\n\tif (head != \"sm\" && head != \"nm\" && head != \"so\") return null\n\tlet num = readRun(chars, i + 2, DIGITS)\n\tif (num.value.len < 1) return null\n\tlet label = sub(chars, i, num.next)\n\t{\n\t\tlabel: label\n\t\turl: `https://www.nicovideo.jp/watch/{label}`\n\t\tnext: num.next\n\t}\n}\n\n// JVN 系は前方一致が長いものから試す\n@matchJvn(chars, i) {\n\tvar hit = matchJvndb(chars, i)\n\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVNVU\", \"vu\")\n\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVNTA\", \"ta\")\n\tif (hit == null) hit = matchJvnFamily(chars, i, \"JVN\", \"jp\")\n\thit\n}\n\n// --- 変換対象外の判定 (候補ごとに前後を見る) ---\n\n// [ラベル](URL) の終端。成立しなければ null\n@readMdLink(chars, i) {\n\tvar j = i + 1\n\tloop {\n\t\tif (j >= chars.len) return null\n\t\tlet c = chars[j]\n\t\tif (c == \"]\") break\n\t\tif (c == Str:lf) return null\n\t\tj += 1\n\t}\n\tif (j + 1 >= chars.len || chars[j + 1] != \"(\") return null\n\tvar k = j + 2\n\tloop {\n\t\tif (k >= chars.len) return null\n\t\tlet c = chars[k]\n\t\tif (c == \")\") break\n\t\tif (isBreak(c)) return null\n\t\tk += 1\n\t}\n\tk + 1\n}\n\n// 空白区切りの直前部分 (候補を含むトークンの先頭から候補まで)\n@tokenBefore(before) {\n\tlet a = before.split(Str:lf)\n\tlet b = a[a.len - 1].split(\" \")\n\tlet c = b[b.len - 1].split(\" \")\n\tc[c.len - 1]\n}\n\n// start〜next の範囲をリンクにしてよいか\n@isLinkable(chars, start, next) {\n\t// 前後に英数字が隣接しているものは語の一部とみなす\n\tif (start > 0 && isWordChar(chars[start - 1])) return false\n\tif (next < chars.len && isWordChar(chars[next])) return false\n\tlet before = sub(chars, 0, start)\n\t// ` の数が奇数ならコードの中 (``` も 1 文字ずつ数えて辻褄が合う)\n\tif ((before.split(\"`\").len - 1) % 2 == 1) return false\n\t// URL / ハッシュタグを含むトークンの中\n\tlet tok = tokenBefore(before)\n\tif (tok.incl(\"https://\") || tok.incl(\"http://\")) return false\n\tif (tok.starts_with(\"#\")) return false\n\t// 同じ行の [ラベル](URL) の中\n\tlet lines = before.split(Str:lf)\n\tlet lineStart = start - lines[lines.len - 1].split().len\n\tvar q = chars.index_of(\"[\", lineStart)\n\tloop {\n\t\tif (q < 0 || q >= start) break\n\t\tlet end = readMdLink(chars, q)\n\t\tif (end != null && end > start) return false\n\t\tq = chars.index_of(\"[\", q + 1)\n\t}\n\ttrue\n}\n\n// 候補位置 p で記法 (matcher) が成立し、変換してよければ hits に積む\n@collect(hits, chars, p, matcher) {\n\tif (p > 0 && isWordChar(chars[p - 1])) return null\n\tlet hit = matcher(chars, p)\n\tif (hit == null) return null\n\tif (!isLinkable(chars, p, hit.next)) return null\n\thits.push({ start: p, next: hit.next, label: hit.label, url: hit.url })\n}\n\n// 接頭辞の無いコミットハッシュ (SHA-1、64 桁の SHA-256 などは長さで弾く) は、16 進がちょうど\n// 40 文字連なる位置を候補にする。40 文字の連なりは 40 文字おきの標本 (from+39, from+79, …) の\n// どれかを必ず含むので、標本が 16 進のときだけ前後に広げて連なりの範囲を求める\n// (本文全体を 1 文字ずつ見ない)。from 以降で最初の連なり { s, e } を返す。無ければ null\n@nextHashRun(chars, from) {\n\tlet n = chars.len\n\tvar j = from + 39\n\tloop {\n\t\tif (j >= n) break\n\t\tif (HEXCHARS.incl(chars[j])) {\n\t\t\tvar s = j\n\t\t\tloop {\n\t\t\t\tif (s > from && HEXCHARS.incl(chars[s - 1])) s -= 1\n\t\t\t\telse break\n\t\t\t}\n\t\t\tvar e = j + 1\n\t\t\tloop {\n\t\t\t\tif (e < n && HEXCHARS.incl(chars[e])) e += 1\n\t\t\t\telse break\n\t\t\t}\n\t\t\tif (e - s == 40) return { s: s, e: e }\n\t\t\t// 次の連なりは e + 1 以降にしか始まらない\n\t\t\tj = e + 40\n\t\t} else {\n\t\t\tj += 40\n\t\t}\n\t}\n\tnull\n}\n\n// 1 回の変換で処理する上限。同じ記法を数百個並べた本文でもステップ上限に届かないようにする。\n// 本文を CHUNK 文字ずつ区切り、区間内の候補を全記法ぶん集めて出現順に並べてから上限を当てる\n// (記法ごとに打ち切ると、後方の記法が先頭の候補を押し出してしまう)\nlet MAX_LINKS = 100\nlet CHUNK = 200\n\n@convertLinks(text, opts) {\n\tlet chars = text.split()\n\t// 大文字小文字を無視する記法は小文字化した本文で探す (lower で長さが変わる文字があれば諦めて原文で探す)\n\tvar lowered = text.lower()\n\tif (lowered.len != text.len) lowered = text\n\n\t// 記法ごとの検索カーソル。優先順 (同じ位置に複数成立したときの順) に並べる\n\t// (str.index_of と split() はどちらもコードポイント単位なので添字を混ぜてよい)\n\tvar sources = []\n\t@addSource(haystack, needle, matcher) {\n\t\tlet pos = haystack.index_of(needle, 0)\n\t\tif (pos >= 0) sources.push({ haystack: haystack, needle: needle, matcher: matcher, pos: pos })\n\t}\n\tif (opts.security) {\n\t\taddSource(lowered, \"jvn\", matchJvn)\n\t\taddSource(lowered, \"cve-\", matchCve)\n\t}\n\tif (opts.dlsite) {\n\t\teach (let needle, [\"RJ\", \"RE\", \"VJ\", \"VE\", \"BJ\", \"BE\", \"RG\"]) addSource(text, needle, matchDlsite)\n\t}\n\tif (opts.nicovideo) {\n\t\teach (let needle, [\"sm\", \"nm\", \"so\"]) addSource(text, needle, matchNico)\n\t}\n\tvar hashRun = if (opts.githubCommit) nextHashRun(chars, 0) else null\n\n\tvar hits = []\n\tvar lastNext = 0\n\tvar a = 0\n\tloop {\n\t\tif (a >= chars.len || hits.len >= MAX_LINKS) break\n\t\tlet b = a + CHUNK\n\t\tvar chunk = []\n\t\teach (let src, sources) {\n\t\t\tloop {\n\t\t\t\tif (src.pos < 0 || src.pos >= b) break\n\t\t\t\tcollect(chunk, chars, src.pos, src.matcher)\n\t\t\t\tsrc.pos = src.haystack.index_of(src.needle, src.pos + 1)\n\t\t\t}\n\t\t}\n\t\tloop {\n\t\t\tif (hashRun == null || hashRun.s >= b) break\n\t\t\tlet hex = sub(chars, hashRun.s, hashRun.e)\n\t\t\tlet e = hashRun.e\n\t\t\tcollect(chunk, chars, hashRun.s, @(c, p) {\n\t\t\t\t{ label: hex, url: `https://github.com/search?q={hex}&type=commits`, next: e }\n\t\t\t})\n\t\t\thashRun = nextHashRun(chars, e + 1)\n\t\t}\n\t\t// 出現順に並べ、重なるもの (同じ位置に複数の記法が成立した場合など) は先のものを優先する\n\t\tif (chunk.len > 1) chunk.sort(@(x, y) { x.start - y.start })\n\t\teach (let hit, chunk) {\n\t\t\tif (hits.len < MAX_LINKS && hit.start >= lastNext) {\n\t\t\t\thits.push(hit)\n\t\t\t\tlastNext = hit.next\n\t\t\t}\n\t\t}\n\t\ta = b\n\t}\n\n\tvar parts = []\n\tvar segStart = 0\n\teach (let hit, hits) {\n\t\tparts.push(sub(chars, segStart, hit.start))\n\t\tparts.push(`[{hit.label}]({hit.url})`)\n\t\tsegStart = hit.next\n\t}\n\tparts.push(sub(chars, segStart, chars.len))\n\t{\n\t\ttext: parts.join(\"\")\n\t\tcount: hits.len\n\t}\n}\n\n@currentOpts() {\n\t{\n\t\tsecurity: Plugin:config.security\n\t\tdlsite: Plugin:config.dlsite\n\t\tnicovideo: Plugin:config.nicovideo\n\t\tgithubCommit: Plugin:config.githubCommit\n\t}\n}\n\nPlugin:register_post_form_action(\"リンクに変換\", @(form, update) {\n\tlet text = if (form.text == null) \"\" else form.text\n\tlet result = convertLinks(text, currentOpts())\n\tif (result.count == 0) {\n\t\tMk:dialog(\"技術系オートリンク\", \"リンクにできる記法が見つかりません\", \"info\")\n\t\treturn null\n\t}\n\tupdate(\"text\", result.text)\n\tMk:toast(`{result.count} 件をリンクにしました`, \"success\")\n})\n\nPlugin:register_note_post_interruptor(@(note) {\n\tif (Plugin:config.autoConvert && note.text != null) {\n\t\tnote.text = convertLinks(note.text, currentOpts()).text\n\t}\n\tnote\n})\n"} diff --git a/public/registry/plugins/tech-autolink/meta.json b/public/registry/plugins/tech-autolink/meta.json index a3ba4a5..c4962e1 100644 --- a/public/registry/plugins/tech-autolink/meta.json +++ b/public/registry/plugins/tech-autolink/meta.json @@ -1,7 +1,7 @@ { "id": "tech-autolink", "name": "技術系オートリンク", - "version": "1.0.0", + "version": "1.1.0", "author": "hitalin", "authorUrl": "https://github.com/hitalin", "description": "CVE / JVN / JVNVU / JVNTA / JVNDB / DLsite / ニコニコ動画 / GitHub コミットハッシュを投稿時に自動でリンクにするプラグイン。misskey-tempura の自動リンク化を、リモートにも届く Markdown リンクとして再現したもの", diff --git a/public/registry/plugins/tech-autolink/plugin.is b/public/registry/plugins/tech-autolink/plugin.is index 8cfc0bb..068be92 100644 --- a/public/registry/plugins/tech-autolink/plugin.is +++ b/public/registry/plugins/tech-autolink/plugin.is @@ -11,10 +11,14 @@ // - コード (` / ```) の中 // - URL の中、Markdown リンク ([ラベル](URL)) の中、ハッシュタグ // - 前後に英数字が隣接しているもの (XCVE-2024-1234 / CVE-2024-1234x など) +// +// 本文を 1 文字ずつ舐めると 1000 字前後で maxStep (100k) を超えるので、 +// 各記法の先頭文字列 (CVE- / JVN / RJ / sm …) の出現位置だけを str.index_of で拾い、 +// その前後だけを検査する。コミットハッシュだけは接頭辞が無いので 16 進の連なりを数える。 ### { name: "技術系オートリンク" - version: "1.0.0" + version: "1.1.0" author: "@hitalin" description: "CVE / JVN / JVNVU / JVNTA / JVNDB / DLsite / ニコニコ動画 / GitHub コミットハッシュを投稿時に自動でリンクにします" config: { @@ -80,13 +84,9 @@ let WORDCHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" // chars の i 文字目から word が (大文字小文字を無視して) 続くか @startsWith(chars, i, word) { - let w = word.split() - if (i + w.len > chars.len) return false - var ok = true - for (let k, w.len) { - if (chars[i + k].lower() != w[k].lower()) ok = false - } - ok + let n = word.split().len + if (i + n > chars.len) return false + sub(chars, i, i + n).lower() == word.lower() } // --- 各記法のマッチャー: 成立すれば { label, url, next }、しなければ null --- @@ -179,34 +179,16 @@ let WORDCHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" } } -// 40 桁ちょうどの 16 進 (SHA-1)。64 桁の SHA-256 などは長さで弾かれる -@matchCommitHash(chars, i) { - let hex = readRun(chars, i, HEXCHARS) - if (hex.value.len != 40) return null - { - label: hex.value - url: `https://github.com/search?q={hex.value}&type=commits` - next: hex.next - } -} - -// 前方一致が長いものから試す -@matchAny(chars, i, opts) { - var hit = null - if (opts.security) { - if (hit == null) hit = matchJvndb(chars, i) - if (hit == null) hit = matchJvnFamily(chars, i, "JVNVU", "vu") - if (hit == null) hit = matchJvnFamily(chars, i, "JVNTA", "ta") - if (hit == null) hit = matchJvnFamily(chars, i, "JVN", "jp") - if (hit == null) hit = matchCve(chars, i) - } - if (opts.dlsite && hit == null) hit = matchDlsite(chars, i) - if (opts.nicovideo && hit == null) hit = matchNico(chars, i) - if (opts.githubCommit && hit == null) hit = matchCommitHash(chars, i) +// JVN 系は前方一致が長いものから試す +@matchJvn(chars, i) { + var hit = matchJvndb(chars, i) + if (hit == null) hit = matchJvnFamily(chars, i, "JVNVU", "vu") + if (hit == null) hit = matchJvnFamily(chars, i, "JVNTA", "ta") + if (hit == null) hit = matchJvnFamily(chars, i, "JVN", "jp") hit } -// --- 変換対象外の範囲を読み飛ばすための判定 --- +// --- 変換対象外の判定 (候補ごとに前後を見る) --- // [ラベル](URL) の終端。成立しなければ null @readMdLink(chars, i) { @@ -230,68 +212,154 @@ let WORDCHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" k + 1 } -// 空白までを 1 塊として読み飛ばす終端 (URL / ハッシュタグ用) -@readUntilBreak(chars, i) { - var j = i +// 空白区切りの直前部分 (候補を含むトークンの先頭から候補まで) +@tokenBefore(before) { + let a = before.split(Str:lf) + let b = a[a.len - 1].split(" ") + let c = b[b.len - 1].split(" ") + c[c.len - 1] +} + +// start〜next の範囲をリンクにしてよいか +@isLinkable(chars, start, next) { + // 前後に英数字が隣接しているものは語の一部とみなす + if (start > 0 && isWordChar(chars[start - 1])) return false + if (next < chars.len && isWordChar(chars[next])) return false + let before = sub(chars, 0, start) + // ` の数が奇数ならコードの中 (``` も 1 文字ずつ数えて辻褄が合う) + if ((before.split("`").len - 1) % 2 == 1) return false + // URL / ハッシュタグを含むトークンの中 + let tok = tokenBefore(before) + if (tok.incl("https://") || tok.incl("http://")) return false + if (tok.starts_with("#")) return false + // 同じ行の [ラベル](URL) の中 + let lines = before.split(Str:lf) + let lineStart = start - lines[lines.len - 1].split().len + var q = chars.index_of("[", lineStart) loop { - if (j >= chars.len) break - if (isBreak(chars[j])) break - j += 1 + if (q < 0 || q >= start) break + let end = readMdLink(chars, q) + if (end != null && end > start) return false + q = chars.index_of("[", q + 1) + } + true +} + +// 候補位置 p で記法 (matcher) が成立し、変換してよければ hits に積む +@collect(hits, chars, p, matcher) { + if (p > 0 && isWordChar(chars[p - 1])) return null + let hit = matcher(chars, p) + if (hit == null) return null + if (!isLinkable(chars, p, hit.next)) return null + hits.push({ start: p, next: hit.next, label: hit.label, url: hit.url }) +} + +// 接頭辞の無いコミットハッシュ (SHA-1、64 桁の SHA-256 などは長さで弾く) は、16 進がちょうど +// 40 文字連なる位置を候補にする。40 文字の連なりは 40 文字おきの標本 (from+39, from+79, …) の +// どれかを必ず含むので、標本が 16 進のときだけ前後に広げて連なりの範囲を求める +// (本文全体を 1 文字ずつ見ない)。from 以降で最初の連なり { s, e } を返す。無ければ null +@nextHashRun(chars, from) { + let n = chars.len + var j = from + 39 + loop { + if (j >= n) break + if (HEXCHARS.incl(chars[j])) { + var s = j + loop { + if (s > from && HEXCHARS.incl(chars[s - 1])) s -= 1 + else break + } + var e = j + 1 + loop { + if (e < n && HEXCHARS.incl(chars[e])) e += 1 + else break + } + if (e - s == 40) return { s: s, e: e } + // 次の連なりは e + 1 以降にしか始まらない + j = e + 40 + } else { + j += 40 + } } - j + null } +// 1 回の変換で処理する上限。同じ記法を数百個並べた本文でもステップ上限に届かないようにする。 +// 本文を CHUNK 文字ずつ区切り、区間内の候補を全記法ぶん集めて出現順に並べてから上限を当てる +// (記法ごとに打ち切ると、後方の記法が先頭の候補を押し出してしまう) +let MAX_LINKS = 100 +let CHUNK = 200 + @convertLinks(text, opts) { let chars = text.split() - var buf = "" - var count = 0 - var i = 0 - // ` の数でコード内かどうかを判定する (``` も 1 文字ずつ反転して辻褄が合う) - var inCode = false - loop { - if (i >= chars.len) break - let ch = chars[i] + // 大文字小文字を無視する記法は小文字化した本文で探す (lower で長さが変わる文字があれば諦めて原文で探す) + var lowered = text.lower() + if (lowered.len != text.len) lowered = text - if (ch == "`") { - inCode = !inCode - buf = `{buf}{ch}` - i += 1 - } elif (inCode) { - buf = `{buf}{ch}` - i += 1 - } elif (ch == "[" && readMdLink(chars, i) != null) { - let end = readMdLink(chars, i) - buf = `{buf}{sub(chars, i, end)}` - i = end - } elif (startsWith(chars, i, "https://") || startsWith(chars, i, "http://")) { - let end = readUntilBreak(chars, i) - buf = `{buf}{sub(chars, i, end)}` - i = end - } elif (ch == "#" && (i == 0 || isBreak(chars[i - 1]))) { - let end = readUntilBreak(chars, i) - buf = `{buf}{sub(chars, i, end)}` - i = end - } elif (i > 0 && isWordChar(chars[i - 1])) { - // 語の途中からは拾わない - buf = `{buf}{ch}` - i += 1 - } else { - let hit = matchAny(chars, i, opts) - // 記法の直後に英数字が続くものは誤検出とみなす - let tail = if (hit == null || hit.next >= chars.len) "" else chars[hit.next] - if (hit == null || isWordChar(tail)) { - buf = `{buf}{ch}` - i += 1 - } else { - buf = `{buf}[{hit.label}]({hit.url})` - count += 1 - i = hit.next + // 記法ごとの検索カーソル。優先順 (同じ位置に複数成立したときの順) に並べる + // (str.index_of と split() はどちらもコードポイント単位なので添字を混ぜてよい) + var sources = [] + @addSource(haystack, needle, matcher) { + let pos = haystack.index_of(needle, 0) + if (pos >= 0) sources.push({ haystack: haystack, needle: needle, matcher: matcher, pos: pos }) + } + if (opts.security) { + addSource(lowered, "jvn", matchJvn) + addSource(lowered, "cve-", matchCve) + } + if (opts.dlsite) { + each (let needle, ["RJ", "RE", "VJ", "VE", "BJ", "BE", "RG"]) addSource(text, needle, matchDlsite) + } + if (opts.nicovideo) { + each (let needle, ["sm", "nm", "so"]) addSource(text, needle, matchNico) + } + var hashRun = if (opts.githubCommit) nextHashRun(chars, 0) else null + + var hits = [] + var lastNext = 0 + var a = 0 + loop { + if (a >= chars.len || hits.len >= MAX_LINKS) break + let b = a + CHUNK + var chunk = [] + each (let src, sources) { + loop { + if (src.pos < 0 || src.pos >= b) break + collect(chunk, chars, src.pos, src.matcher) + src.pos = src.haystack.index_of(src.needle, src.pos + 1) } } + loop { + if (hashRun == null || hashRun.s >= b) break + let hex = sub(chars, hashRun.s, hashRun.e) + let e = hashRun.e + collect(chunk, chars, hashRun.s, @(c, p) { + { label: hex, url: `https://github.com/search?q={hex}&type=commits`, next: e } + }) + hashRun = nextHashRun(chars, e + 1) + } + // 出現順に並べ、重なるもの (同じ位置に複数の記法が成立した場合など) は先のものを優先する + if (chunk.len > 1) chunk.sort(@(x, y) { x.start - y.start }) + each (let hit, chunk) { + if (hits.len < MAX_LINKS && hit.start >= lastNext) { + hits.push(hit) + lastNext = hit.next + } + } + a = b + } + + var parts = [] + var segStart = 0 + each (let hit, hits) { + parts.push(sub(chars, segStart, hit.start)) + parts.push(`[{hit.label}]({hit.url})`) + segStart = hit.next } + parts.push(sub(chars, segStart, chars.len)) { - text: buf - count: count + text: parts.join("") + count: hits.len } } diff --git a/public/registry/queries.json b/public/registry/queries.json index c8d482e..5de59f3 100644 --- a/public/registry/queries.json +++ b/public/registry/queries.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "queries": [ { "id": "bot-only", diff --git a/public/registry/skills.json b/public/registry/skills.json index ecee07c..e2f7f94 100644 --- a/public/registry/skills.json +++ b/public/registry/skills.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "skills": [ { "id": "aiscript-surgeon", diff --git a/public/registry/themes.json b/public/registry/themes.json index 8fda2ae..eff471d 100644 --- a/public/registry/themes.json +++ b/public/registry/themes.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "themes": [ { "id": "adwaita-dark", diff --git a/public/registry/widgets.json b/public/registry/widgets.json index 8fec00b..00cb9b6 100644 --- a/public/registry/widgets.json +++ b/public/registry/widgets.json @@ -1,6 +1,6 @@ { "version": 1, - "updatedAt": "2026-10-28T19:35:34.231Z", + "updatedAt": "2026-10-28T23:08:30.161Z", "widgets": [ { "id": "aiscript-health", @@ -423,8 +423,8 @@ "sourceUrl": "https://store.notedeck.io/registry/widgets/discord-presence/widget.is", "apiUrl": "https://store.notedeck.io/registry/widgets/discord-presence/api.json", "sha512": "2b682319b8751ed771e26cc7c441b65182dad51530751e32cf5374ae8d8e18617bd461301c607f32f4db48621006e33dc1da722ca8124f87535a21916f1de4e6", - "createdAt": "2026-10-28T19:35:37.313Z", - "updatedAt": "2026-10-28T19:35:37.313Z", + "createdAt": "2026-10-29T04:15:03+09:00", + "updatedAt": "2026-10-29T04:25:33+09:00", "authorUrl": "https://github.com/hitalin", "license": "MIT", "locales": {