Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions public/registry/index.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
10 changes: 5 additions & 5 deletions public/registry/plugins.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-10-28T19:35:34.231Z",
"updatedAt": "2026-10-28T23:08:30.161Z",
"plugins": [
{
"id": "ai-actions",
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand All @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion public/registry/plugins/tech-autolink/api.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion public/registry/plugins/tech-autolink/meta.json
Original file line number Diff line number Diff line change
@@ -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 リンクとして再現したもの",
Expand Down
236 changes: 152 additions & 84 deletions public/registry/plugins/tech-autolink/plugin.is
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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 ---
Expand Down Expand Up @@ -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) {
Expand All @@ -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
}
}

Expand Down
2 changes: 1 addition & 1 deletion public/registry/queries.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-10-28T19:35:34.231Z",
"updatedAt": "2026-10-28T23:08:30.161Z",
"queries": [
{
"id": "bot-only",
Expand Down
2 changes: 1 addition & 1 deletion public/registry/skills.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-10-28T19:35:34.231Z",
"updatedAt": "2026-10-28T23:08:30.161Z",
"skills": [
{
"id": "aiscript-surgeon",
Expand Down
2 changes: 1 addition & 1 deletion public/registry/themes.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"updatedAt": "2026-10-28T19:35:34.231Z",
"updatedAt": "2026-10-28T23:08:30.161Z",
"themes": [
{
"id": "adwaita-dark",
Expand Down
Loading
Loading