Nrem/UserScript
Nrem.iconNrem
Nrem/UserCSS
#UserScript
code:UseScript.js
import '/api/code/nremiel/Nrem/UserScript/script.js';
/customize/UserScript
/icons/hrd.icon
/customize/icon-suggestion
/customize/箇条書きに番号を付けるUserScript
文字カウント
/customize/文字カウント
code:script.js
import 'https://scrapbox.io/api/code/customize/文字カウント/script.js';
/customize/文字列を選択してScrapbox内検索
文字列を選択してScrapbox内検索
https://i.gyazo.com/a640bd1ba8436b74549351c7d3ae3bc3.gif
code:script.js
// 選択された文字列をScrapboxプロジェクト内で検索する
// Scapbox検索ボックスを使ったときと同じ結果ページを開く
scrapbox.PopupMenu.addButton({
title: 'スクボ内検索',
onClick: function (text) {
var projectName = 'daiiz';
window.open('https://scrapbox.io/'+ projectName +'/search/page?q=' + text);
}
});
Emoji selector
/yutaro/emoji selector
code:script.js
import '/api/code/yutaro/emoji_selector/script.js';
Hierarchy Extension
/customize/Hierarchy Extension
code:script.js
import '/api/code/customize/Hierarchy_Extension/script.js';
HEX形式の色表記に色プレビューを挿入する
改変元:/customize/RGBの表記を見つけて色プレビューを挿入する
元の版は""で囲まれていなければ対象にならない。
HEXはコード記法で囲むのがCosenseにおける一般的な表記だろう。
""の有無に限らず、コード記法内のHEX全てが対象になるように変更。
緋色 "#D3381C"
黄丹 "#EE7948"
黄金 "#E6B422"
翡翠色 "#38B48B"
露草色 "#38A1DB"
露草色 #38A1DB
菫色 "#7065A3"
躑躅色 "#E95295"
code:sample.css
躑躅色 "#E95295"
躑躅色 #E95295
躑躅色 #E95295
code:script.js
(() => {
'use strict';
// ========= 設定 =========
const RE = /#0-9a-fA-F{3}(?:0-9a-fA-F{3})?/g; // " #RGB " or " #RRGGBB "
const STYLE_ID = 'sbx-hexswatch-style';
const LINE_CANDIDATES = ['.page .line', '.page data-line', '.page class*="line"'];
const LINE_SELECTOR = LINE_CANDIDATES.join(',');
const POLL_MS = 250; // 軽量ポーリング間隔
const LINE_EVENTS = 'input', 'keyup', 'paste', 'cut', 'compositionend';
// ========= スタイル =========
const ensureStyle = () => {
if (document.getElementById(STYLE_ID)) return;
const s = document.createElement('style');
s.id = STYLE_ID;
s.textContent = `
.sbx-swatch{
display:inline-block;
width:0.9em; height:0.9em;
margin-right:0.1em; vertical-align:-0.1em;
border:1px solid rgba(0,0,0,.28); border-radius:2px;
background:var(--sbx-color,#000); box-sizing:border-box;
}
`;
document.head.appendChild(s);
};
// ========= ユーティリティ =========
const expand3 = (hex) => hex.length === 4
? '#'+hex1+hex1+hex2+hex2+hex3+hex3
: hex;
// TextNode連結 & offset→(node,off) 解決(resolveは二分探索)
const buildTextAndResolver = (container) => {
const w = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
acceptNode: (n) => n.textContent ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT
});
const nodes = [], lens = [], parts = [];
for (let n; (n = w.nextNode()); ) {
const t = n.textContent;
if (!t) continue;
nodes.push(n); lens.push(t.length); parts.push(t);
}
const text = parts.join('');
const total = lens.reduce((a,b)=>a+b,0);
if (!nodes.length) return { text, total, resolve: () => ({ node:null, offset:0 }) };
// 累積和
const cumul = new Array(lens.length);
let acc = 0;
for (let i=0; i<lens.length; i++) { acc += lensi; cumuli = acc; }
const resolve = (offset) => {
if (offset <= 0) return { node:nodes0, offset:0 };
if (offset >= total) return { node:nodesnodes.length-1, offset:lenslens.length-1 };
// 二分探索で cumulidx > offset となる最小idxを探す
let lo = 0, hi = cumul.length - 1, idx = hi;
while (lo <= hi) {
const mid = (lo + hi) >> 1;
if (cumulmid > offset) { idx = mid; hi = mid - 1; }
else { lo = mid + 1; }
}
const prev = idx > 0 ? cumulidx-1 : 0;
return { node:nodesidx, offset: offset - prev };
};
return { text, total, resolve };
};
// ========= 1行処理(全スウォッチ削除 → 再検出 → 先頭位置にだけ挿入) =========
const processLine = (lineEl) => {
if (!(lineEl instanceof Element)) return;
// 既存スウォッチ全削除(直下/子孫ともにqSAで十分)
lineEl.querySelectorAll('.sbx-swatch').forEach(s => s.remove());
const { text, total, resolve } = buildTextAndResolver(lineEl);
if (!text || total === 0) return;
if (text.indexOf('"#') === -1) return;
let m; RE.lastIndex = 0;
while ((m = RE.exec(text)) !== null) {
const start = m.index;
const quoted = m0; // "\"#FFAABB\""
const rawHex = quoted.slice(1, -1); // -> #FFAABB
const hex = expand3(rawHex).toUpperCase();
const a = resolve(start);
if (!a.node) continue;
try {
const r = document.createRange();
r.setStart(a.node, a.offset);
r.setEnd(a.node, a.offset);
const sw = document.createElement('span');
sw.className = 'sbx-swatch';
sw.style.setProperty('--sbx-color', hex);
sw.title = rawHex;
r.insertNode(sw);
} catch { /* ignore */ }
}
};
// ========= 差分監視(イベント + 軽量ポーリング) =========
let currentPage = null;
let mo = null;
let pollId = null;
const lastTextCache = new WeakMap(); // lineEl -> lastText
const processIfChanged = (line) => {
const { text } = buildTextAndResolver(line);
const prev = lastTextCache.get(line);
if (prev !== text) {
lastTextCache.set(line, text);
processLine(line);
}
};
const addLineListeners = (el) => {
const h = () => processIfChanged(el);
for (const ev of LINE_EVENTS) el.addEventListener?.(ev, h);
// 初期キャッシュ & 初期処理
const { text } = buildTextAndResolver(el);
lastTextCache.set(el, text);
processLine(el);
};
const startPolling = () => {
if (pollId) return;
pollId = setInterval(() => {
try {
currentPage?.querySelectorAll?.(LINE_SELECTOR)?.forEach(processIfChanged);
} catch { /* ignore */ }
}, POLL_MS);
};
const stopPolling = () => {
if (pollId) { clearInterval(pollId); pollId = null; }
};
const attach = () => {
const page = document.querySelector('.page');
if (!page || page === currentPage) return;
// 既存監視をクリア
if (mo) { mo.disconnect(); mo = null; }
stopPolling();
lastTextCache.clear?.();
currentPage = page;
// 既存行:初期処理 + 主要イベント
page.querySelectorAll(LINE_SELECTOR).forEach(addLineListeners);
// MutationObserver(追加/文字差し替え)
const root = page.querySelector('.lines') || page;
mo = new MutationObserver((muts) => {
for (const m of muts) {
if (m.type === 'characterData') {
const line = m.target.parentElement?.closest?.(LINE_SELECTOR);
if (line) processIfChanged(line);
}
if (m.addedNodes && m.addedNodes.length) {
m.addedNodes.forEach(n => {
if (n.nodeType !== 1) return;
if (n.matches?.(LINE_SELECTOR)) addLineListeners(n);
n.querySelectorAll?.(LINE_SELECTOR)?.forEach(addLineListeners);
});
}
}
});
mo.observe(root, { characterData:true, childList:true, subtree:true });
// 軽量ポーリング
if (document.visibilityState === 'visible') startPolling();
};
// ========= PJAX 追随(History API + 保険) =========
const installNavHooks = () => {
const fire = () => window.dispatchEvent(new Event('sbx:navigate'));
const wrap = (t) => {
const orig = historyt;
return function(...args){ const ret = orig.apply(this, args); fire(); return ret; };
};
history.pushState = wrap('pushState');
history.replaceState = wrap('replaceState');
window.addEventListener('popstate', fire);
window.addEventListener('sbx:navigate', () => {
let tries = 0;
const retry = () => {
const pageNow = document.querySelector('.page');
if (pageNow && pageNow !== currentPage) { attach(); return; }
if (tries++ < 20) requestAnimationFrame(retry);
};
requestAnimationFrame(retry);
});
new MutationObserver(() => attach()).observe(document.body, { childList:true, subtree:true });
// タブの可視状態でポーリングを抑制
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') startPolling();
else stopPolling();
});
window.addEventListener('pageshow', () => attach());
};
// ========= 起動 =========
const boot = () => { ensureStyle(); installNavHooks(); attach(); };
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot);
else boot();
})();
行頭に引用符をつける
/shokai/行頭に引用符を付けるUserScript
code:script.js
import 'https://scrapbox.io/api/code/shokai/%E8%A1%8C%E9%A0%AD%E3%81%AB%E5%BC%95%E7%94%A8%E7%AC%A6%E3%82%92%E4%BB%98%E3%81%91%E3%82%8BUserScript/script.js';
外部プロジェクトのリンクを入力補完出来るようにする
/customize/external-completion
Scrapboxのコードブロック記法で空白文字可視化
/yuiseki/Scrapboxのコードブロック記法で空白文字可視化
status barに文字カウントを表示する(event版)
? 有効にしても動作せず、UserScript全体が動作しなくなる
/shokai/status barに文字カウントを表示する(event版)
ポップアップメニュー
外部プロジェクトのリンクを入力補完出来るようにする
/customize/external-completion
code:script.js
import {startSuggestingExternalProjectLinks}
from '/api/code/customize/external-completion/script.js';
// 入力候補に入れたいprojectを書く
startSuggestingExternalProjectLinks([
'nremiel',
'nremiel-privata',
'masui',
'shokai',
'hub',
'villagepump',
'customize',
'scrapboxlab',
'scrasobox',
'icons',
'icons2',
'emoji',
'help-jp',
'help',
'help-eo-neoficiala']);
/customize/Mermaid記法可視化userscript
code:script.js
import "/api/code/customize/Mermaid記法可視化userscript/script.js";
ページメニューボタンの拡張
? scrapbox.PageMenu.addMenu()を複数拡張すると競合して動かなくなる可能性あり?
階層構造が見やすい見出しを出すユーザースクリプト
/quolc-public/quolc#5b1b5e638595d100006fa17a
code:script.js
/* (function () { */
scrapbox.PageMenu.addMenu({
title: '見出し',
image: 'https://gyazo.com/bc38721e0980f2188f1c831754ac8da4/raw',
onClick: () => {
scrapbox.PageMenu('見出し').removeAllItems()
var deepest = 0;
for (let line of scrapbox.Page.lines) {
if (!line.section.start) continue
var depth = line.nodes ? calcDepth(line.nodes) : 0
if (deepest < depth) deepest = depth
}
console.log('deepest index is ' + deepest)
for (let line of scrapbox.Page.lines) {
if (!line.section.start) continue
const image = line.nodes && getIconUrl(line.nodes)
const noIcon = !!image
const title = line.nodes ? renderPlainText(line.nodes, {noIcon, level: 0}, deepest) : line.text
const onClick = () => location.hash = line.id
scrapbox.PageMenu('見出し').addItem({title, image, onClick})
}
}
})
function calcDepth (node) {
if (node instanceof Array) return node.map(node => calcDepth(node)).reduce(function(a,b) {return Math.max(a,b);}, 0)
if (typeof node === 'string') return 0
if (node.type == 'deco') {
var deco = '*'
for (var i=1; i<=5; i++) {
if (node.unit.deco == deco) return i
deco = deco + '*'
}
}
return calcDepth(node.children)
}
function renderPlainText (node, options, deepest) {
if (node instanceof Array) return node.map(node => renderPlainText(node, options, deepest)).join('')
if (typeof node === 'string') {
var indent = deepest - options.level
var item = node
if (options.level == 1) item = '・' + item
if (options.level == 2) item = '● ' + item
if (options.level == 3) item = '■ ' + item
if (options.level == 4) item = '■ ' + item
if (options.level == 5) item = '■ ' + item
for (var i=0; i<indent; i++) item = ' ' + item;
return item
}
var new_options = Object.assign({}, options)
switch (node.type) {
case 'icon':
case 'strong-icon':
return options.noIcon ? ' ' : node.unit.page
case 'deco':
new_options.level = 0
var deco = '*'
for (var i=1; i<=5; i++) {
if (node.unit.deco == deco) new_options.level = i
deco = deco + '*'
}
}
return renderPlainText(node.children, new_options, deepest)
}
function getIconUrl (node) {
if (/icon/.test(node.type)) {
return /api/pages/${node.unit.project||scrapbox.Project.name}/${node.unit.page}/icon
}
if (node instanceof Array) {
return node.map(getIconUrl).find(img => img)
}
return null
}
/* })() */
未読表示されているページのみにランダム表示するPage Menu
cf. /villagepump/未読のページにランダムジャンプするUserScript:ページを開かずに未読を判定するver
code:script.js
import 'https://scrapbox.io/api/code/villagepump/未読のページにランダムジャンプするUserScript:ページを開かずに未読を判定するver/script.js';
どこでも RUN JavaScript Button with HTML&CSS
/customize/どこでも RUN JavaScriptBT with HTML&CSS