すぎぴ
public.icon
コンテンツポリシー
家族・これからの子孫には全て見せても良い内容にする
プロフィール
https://lh3.googleusercontent.com/a/ACg8ocI4UaFaYtP1Q9y8CY1-tZCDwFUgDjLHT_8SLSqL17ekARuE=s96-c#.png
https://gyazo.com/86fdce7272f87d27fbef34af355df241
趣味
スマブラ
ウルフ 段位:地元最強 スマメイト最高R1603
美味しい食べ物
基本的にスパイスカレーが中心です。下北沢と渋谷を中心に新しいお店を開拓しています。
登山
登山日記
筋トレ
なぜ筋トレをするのか / 筋トレ計画
簡単な経歴
97年藤沢市生誕→県立希望ヶ丘高校卒業→立命館アジア太平洋大学→サイバー大学編入→ベンチャー企業で3年間修行→スタートアップで独立→事業撤退→モラトリアム(1年自分の人生模索)→家業のテニススクール経営・YouTubeディレクター
生活圏
別府→渋谷→神奈川湘南・五反田・下北沢
好きな〇〇
好きな本・影響を受けた本
ピーターティール / ZERO to ONE
岡本太郎 / 自分の中に毒を持て
エーリッヒ・フロム / 『愛するということ』
ref. 今まで読んだ、読書リスト
好きな漫画
HUNTER×HUNTER / 進撃の巨人 / 『チ。』 / さよなら絵梨
浅野いにお
藤本タツキ
魚豊
ref. 今まで読んだ、漫画リスト
好きな映画
『花束みたいな恋をした』
メメント
ref. 今まで見た、映画リスト
好きなアーティスト
好きなアーティスト
Bialystocks
尊敬する人
岡本太郎 / レヴィ=ストロース / 田中角栄 / 朝井リョウ / 魚豊 / 高橋弘樹 / 石原慎太郎
code:style.css
.level-2 {
border-left: 10px solid #1E88E5;
border-bottom: 3px solid #B0BEC5;
}
/* Cosense UserCSS: public.icon を含むカードにグロー効果
使い方: Cosenseプロジェクトの任意のページに code:style.css として貼り付ける
※ CSSだけでは「public.iconを含むカード」を直接セレクトできないため、
JSと併用するのが確実。このCSSはJS側で付与する data-public-glow 属性に対応。
*/
/* JS連携版: data-public-glow="true" が付いたカード */
.page-list-itemdata-public-glow="true" {
box-shadow: 0 0 12px rgba(34, 197, 94, 0.35),
0 0 4px rgba(34, 197, 94, 0.2);
border-color: rgba(34, 197, 94, 0.5) !important;
transition: box-shadow 0.3s ease, border-color 0.3s ease;
}
/* ホバー時にもう少し明るく */
.page-list-itemdata-public-glow="true":hover {
box-shadow: 0 0 20px rgba(34, 197, 94, 0.5),
0 0 6px rgba(34, 197, 94, 0.3);
}
/* --- アニメーション版(お好みで) --- */
/*
@keyframes subtle-pulse {
0%, 100% {
box-shadow: 0 0 10px rgba(34, 197, 94, 0.3),
0 0 3px rgba(34, 197, 94, 0.15);
}
50% {
box-shadow: 0 0 16px rgba(34, 197, 94, 0.45),
0 0 6px rgba(34, 197, 94, 0.25);
}
}
.page-list-itemdata-public-glow="true" {
animation: subtle-pulse 3s ease-in-out infinite;
border-color: rgba(34, 197, 94, 0.5) !important;
}
*/
code:script.js
// Cmd+B / Ctrl+B で選択テキストを ... 強調にトグル
document.addEventListener('keydown', (e) => {
if (!(e.metaKey || e.ctrlKey) || e.key !== 'b') return;
const selection = window.getSelection();
if (!selection || selection.isCollapsed) return;
const selectedText = selection.toString();
if (!selectedText) return;
e.preventDefault();
e.stopPropagation();
// トグル判定
let newText;
if (selectedText.startsWith('') && selectedText.endsWith('')) {
newText = selectedText.slice(3, -1);
} else {
newText = [* ${selectedText}];
}
// Cosenseの入力方式: #text-input に value をセットして InputEvent を発火
const textArea = document.getElementById('text-input');
// 選択状態のままInputEventを発火すると、選択範囲が置換される
textArea.value = newText;
textArea.dispatchEvent(new InputEvent('input', { bubbles: true, cancelable: true }));
});
code:script.js
console.log('=== public glow script loaded ===');
// Cosense UserScript: public.icon が付いたページカードを光らせる
// 使い方: Cosenseプロジェクトの任意のページに code:script.js として貼り付ける
// public.icon グロースクリプト
// public.icon グロースクリプト
// public.icon グロースクリプト(API版)
// public.icon グロースクリプト(API版)
const PROJECT = scrapbox.Project.name;
const glowCache = {};
async function checkPublicIcon(pageTitle) {
if (glowCachepageTitle !== undefined) return glowCachepageTitle;
try {
const res = await fetch('/api/pages/' + PROJECT + '/' + encodeURIComponent(pageTitle));
const data = await res.json();
const lines = data.lines || [];
const hasPublic = lines.some(line => line.text.includes('public.icon'));
glowCachepageTitle = hasPublic;
return hasPublic;
} catch (e) {
return false;
}
}
async function highlightPublicPages() {
const cards = document.querySelectorAll('.page-list-item');
for (const card of cards) {
if (card.dataset.publicGlow === 'done') continue;
const titleEl = card.querySelector('.title');
if (!titleEl) continue;
const title = titleEl.textContent.trim();
if (!title) continue;
const hasPublic = await checkPublicIcon(title);
if (hasPublic) {
card.style.boxShadow = [
'0 0 20px rgba(34, 197, 94, 0.5)',
'0 0 8px rgba(34, 197, 94, 0.3)',
'0 2px 8px rgba(0, 0, 0, 0.12)',
'inset 0 1px 0 rgba(255, 255, 255, 0.15)'
].join(', ');
card.style.borderColor = 'rgba(34, 197, 94, 0.6)';
card.style.borderWidth = '1.5px';
card.style.transform = 'translateY(-1px)';
card.style.transition = 'box-shadow 0.3s ease, border-color 0.3s ease, transform 0.3s ease';
}
card.dataset.publicGlow = 'done';
}
}
function startPublicGlow() {
const cards = document.querySelectorAll('.page-list-item');
if (cards.length === 0) {
setTimeout(startPublicGlow, 1000);
return;
}
highlightPublicPages();
setInterval(highlightPublicPages, 3000);
document.addEventListener('scroll', () => {
setTimeout(highlightPublicPages, 500);
}, true);
new MutationObserver(() => highlightPublicPages()).observe(
document.body, { childList: true, subtree: true }
);
}
setTimeout(startPublicGlow, 1500);
code:script.js
// ==Cosense Share Card Generator==
// Instagramストーリー用のシェアカード画像を生成するUserScript
// Cosenseページのタイトル・アイコン・プロジェクト名からPNG画像をダウンロード
//
// 使い方: Cosenseのプロフィールページの script.js コードブロックにこのコードを貼り付ける
// 参考: https://scrapbox.io/help/Userscript
(function () {
'use strict';
// ===== 設定 =====
const CONFIG = {
// カードサイズ(Instagramストーリー推奨: 1080x1920)
cardWidth: 1080,
cardHeight: 1920,
// Cosenseブランドカラー
colors: {
bgGradientStart: '#1a1a2e', // 深いダークネイビー
bgGradientEnd: '#0f3460', // ダークブルー
accent: '#4CAF50', // Cosenseグリーン
accentLight: '#81C784', // ライトグリーン
textPrimary: '#FFFFFF',
textSecondary: '#B0BEC5',
cardBg: 'rgba(255, 255, 255, 0.08)',
cardBorder: 'rgba(76, 175, 80, 0.3)',
},
// フォント
fonts: {
title: 'bold 54px "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif',
project: '28px "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif',
url: '22px "Noto Sans JP", monospace',
watermark: '20px "Noto Sans JP", sans-serif',
},
// 本文表示設定
bodyTextMaxChars: 80, // ← ここで表示文字数を変更(0にすると非表示)
bodyTextMaxLines: 4, // 最大行数
// フォント
bodyFont: '26px "Noto Sans JP", "Hiragino Kaku Gothic ProN", sans-serif',
// アイコンサイズ
iconSize: 240,
// ボタンの位置(ページメニューバーに追加)
buttonId: 'cosense-share-card-btn',
};
// ===== ユーティリティ =====
// 現在のプロジェクト名を取得
function getProjectName() {
return window.scrapbox?.Project?.name || location.pathname.split('/')1 || 'unknown';
}
// 現在のページタイトルを取得
function getPageTitle() {
return window.scrapbox?.Page?.title || document.querySelector('.page-title')?.textContent?.trim() || 'Untitled';
}
// ページのアイコン画像URLを取得
function getIconUrl() {
const project = getProjectName();
const title = getPageTitle();
// Cosense API でページアイコンを取得
return https://scrapbox.io/api/pages/${encodeURIComponent(project)}/${encodeURIComponent(title)}/icon;
}
// ページURLを取得
function getPageUrl() {
return location.href;
}
// ページ本文を取得(タイトル行・空行・記法を除去)
function getPageBody(maxChars) {
if (maxChars <= 0) return '';
const lines = window.scrapbox?.Page?.lines || [];
// 1行目はタイトルなのでスキップ
const bodyLines = lines.slice(1)
.map(l => l.text || '')
// Scrapbox記法をざっくり除去
.map(l => l
.replace(/\[(^\]*\.icon)\]/g, '') // xxx.icon
.replace(/\[(^\]*)\]/g, '$1') // リンク → リンク
.replace(/[^]*/g, '') // code`
.replace(/^\s>*\-#+/, '') // 行頭記号
.trim()
)
.filter(l => l.length > 0);
const joined = bodyLines.join(' ');
if (joined.length <= maxChars) return joined;
return joined.slice(0, maxChars) + '…';
}
// 画像を読み込む(CORS対応)
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => resolve(img);
img.onerror = () => reject(new Error(Failed to load image: ${url}));
img.src = url;
});
}
// テキストを折り返し描画(中央揃え or 左揃え)
function wrapText(ctx, text, x, y, maxWidth, lineHeight, options = {}) {
const { maxLines = 3, align = 'center' } = options;
const chars = ...text;
let line = '';
const lines = [];
for (let i = 0; i < chars.length; i++) {
const testLine = line + charsi;
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line.length > 0) {
lines.push(line);
line = charsi;
} else {
line = testLine;
}
}
lines.push(line);
const displayLines = lines.slice(0, maxLines);
if (lines.length > maxLines) {
displayLinesmaxLines - 1 = displayLinesmaxLines - 1.slice(0, -1) + '…';
}
const totalHeight = displayLines.length * lineHeight;
const startY = y - totalHeight / 2 + lineHeight / 2;
displayLines.forEach((l, idx) => {
if (align === 'center') {
const lineWidth = ctx.measureText(l).width;
ctx.fillText(l, x - lineWidth / 2, startY + idx * lineHeight);
} else {
ctx.fillText(l, x, startY + idx * lineHeight);
}
});
return totalHeight;
}
// 角丸四角形を描画
function roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
// 円形にクリッピングして画像を描画
function drawCircularImage(ctx, img, x, y, size) {
ctx.save();
ctx.beginPath();
ctx.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, x, y, size, size);
ctx.restore();
}
// ===== カード画像生成 =====
async function generateShareCard() {
const { cardWidth, cardHeight, colors, fonts, iconSize } = CONFIG;
const projectName = getProjectName();
const pageTitle = getPageTitle();
const pageUrl = getPageUrl();
const bodyText = getPageBody(CONFIG.bodyTextMaxChars);
const canvas = document.createElement('canvas');
canvas.width = cardWidth;
canvas.height = cardHeight;
const ctx = canvas.getContext('2d');
// --- 背景グラデーション ---
const bgGrad = ctx.createLinearGradient(0, 0, 0, cardHeight);
bgGrad.addColorStop(0, colors.bgGradientStart);
bgGrad.addColorStop(1, colors.bgGradientEnd);
ctx.fillStyle = bgGrad;
ctx.fillRect(0, 0, cardWidth, cardHeight);
// --- 装飾: 薄いグリーンの光彩 ---
const glowGrad = ctx.createRadialGradient(
cardWidth / 2, cardHeight * 0.38, 0,
cardWidth / 2, cardHeight * 0.38, 500
);
glowGrad.addColorStop(0, 'rgba(76, 175, 80, 0.12)');
glowGrad.addColorStop(1, 'rgba(76, 175, 80, 0)');
ctx.fillStyle = glowGrad;
ctx.fillRect(0, 0, cardWidth, cardHeight);
// --- カード高さを動的計算(本文がある場合は高くする) ---
const hasBody = bodyText.length > 0;
const cardInnerH = hasBody ? 1060 : 900;
const cardPadding = 60;
const cardInnerW = cardWidth - cardPadding * 2;
const cardX = cardPadding;
const cardY = (cardHeight - cardInnerH) / 2 - 40;
roundRect(ctx, cardX, cardY, cardInnerW, cardInnerH, 32);
ctx.fillStyle = colors.cardBg;
ctx.fill();
ctx.strokeStyle = colors.cardBorder;
ctx.lineWidth = 2;
ctx.stroke();
// --- アイコン画像 ---
const iconX = (cardWidth - iconSize) / 2;
const iconY = cardY + 80;
try {
const iconImg = await loadImage(getIconUrl());
// アイコンの影
ctx.save();
ctx.shadowColor = 'rgba(76, 175, 80, 0.4)';
ctx.shadowBlur = 30;
ctx.beginPath();
ctx.arc(iconX + iconSize / 2, iconY + iconSize / 2, iconSize / 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0,0,0,0.01)';
ctx.fill();
ctx.restore();
// アイコン枠線
ctx.beginPath();
ctx.arc(iconX + iconSize / 2, iconY + iconSize / 2, iconSize / 2 + 4, 0, Math.PI * 2);
const borderGrad = ctx.createLinearGradient(iconX, iconY, iconX + iconSize, iconY + iconSize);
borderGrad.addColorStop(0, colors.accent);
borderGrad.addColorStop(1, colors.accentLight);
ctx.strokeStyle = borderGrad;
ctx.lineWidth = 4;
ctx.stroke();
drawCircularImage(ctx, iconImg, iconX, iconY, iconSize);
} catch (e) {
// アイコン読み込み失敗時: プレースホルダー
ctx.beginPath();
ctx.arc(iconX + iconSize / 2, iconY + iconSize / 2, iconSize / 2, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(76, 175, 80, 0.2)';
ctx.fill();
ctx.strokeStyle = colors.accent;
ctx.lineWidth = 4;
ctx.stroke();
// イニシャル表示
ctx.font = 'bold 96px "Noto Sans JP", sans-serif';
ctx.fillStyle = colors.accent;
ctx.textBaseline = 'middle';
const initial = pageTitle.charAt(0);
const initialWidth = ctx.measureText(initial).width;
ctx.fillText(initial, iconX + iconSize / 2 - initialWidth / 2, iconY + iconSize / 2);
}
// --- ページタイトル ---
ctx.font = fonts.title;
ctx.fillStyle = colors.textPrimary;
ctx.textBaseline = 'middle';
const titleY = iconY + iconSize + 100;
wrapText(ctx, pageTitle, cardWidth / 2, titleY, cardInnerW - 80, 72);
// --- 区切り線 ---
const dividerY = titleY + 100;
const dividerGrad = ctx.createLinearGradient(cardX + 100, 0, cardX + cardInnerW - 100, 0);
dividerGrad.addColorStop(0, 'rgba(76, 175, 80, 0)');
dividerGrad.addColorStop(0.5, 'rgba(76, 175, 80, 0.5)');
dividerGrad.addColorStop(1, 'rgba(76, 175, 80, 0)');
ctx.strokeStyle = dividerGrad;
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(cardX + 100, dividerY);
ctx.lineTo(cardX + cardInnerW - 100, dividerY);
ctx.stroke();
// --- プロジェクト名 ---
ctx.font = fonts.project;
ctx.fillStyle = colors.textSecondary;
ctx.textBaseline = 'middle';
const projectLabel = 📁 ${projectName};
const projectWidth = ctx.measureText(projectLabel).width;
ctx.fillText(projectLabel, (cardWidth - projectWidth) / 2, dividerY + 60);
// --- ページURL(短縮表示) ---
ctx.font = fonts.url;
ctx.fillStyle = colors.accent;
ctx.textBaseline = 'middle';
const shortUrl = cosen.se/${projectName};
const urlWidth = ctx.measureText(shortUrl).width;
ctx.fillText(shortUrl, (cardWidth - urlWidth) / 2, dividerY + 115);
// --- 本文テキスト ---
if (hasBody) {
// 本文エリアの区切り線
const bodyDividerY = dividerY + 165;
const bodyDivGrad = ctx.createLinearGradient(cardX + 140, 0, cardX + cardInnerW - 140, 0);
bodyDivGrad.addColorStop(0, 'rgba(255, 255, 255, 0)');
bodyDivGrad.addColorStop(0.5, 'rgba(255, 255, 255, 0.15)');
bodyDivGrad.addColorStop(1, 'rgba(255, 255, 255, 0)');
ctx.strokeStyle = bodyDivGrad;
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cardX + 140, bodyDividerY);
ctx.lineTo(cardX + cardInnerW - 140, bodyDividerY);
ctx.stroke();
// 本文テキスト描画
ctx.font = CONFIG.bodyFont;
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.textBaseline = 'middle';
const bodyY = bodyDividerY + 80;
const bodyPadding = 100;
wrapText(ctx, bodyText, cardWidth / 2, bodyY, cardInnerW - bodyPadding * 2, 40, {
maxLines: CONFIG.bodyTextMaxLines,
align: 'center',
});
}
// --- 下部ウォーターマーク ---
ctx.font = fonts.watermark;
ctx.fillStyle = 'rgba(255, 255, 255, 0.25)';
ctx.textBaseline = 'bottom';
const watermark = 'Shared from Cosense';
const wmWidth = ctx.measureText(watermark).width;
ctx.fillText(watermark, (cardWidth - wmWidth) / 2, cardHeight - 60);
// --- 上部にCosenseロゴテキスト ---
ctx.font = 'bold 36px "Noto Sans JP", sans-serif';
const logoGrad = ctx.createLinearGradient(0, 50, 0, 90);
logoGrad.addColorStop(0, colors.accent);
logoGrad.addColorStop(1, colors.accentLight);
ctx.fillStyle = logoGrad;
ctx.textBaseline = 'top';
const logoText = '● Cosense';
const logoWidth = ctx.measureText(logoText).width;
ctx.fillText(logoText, (cardWidth - logoWidth) / 2, 80);
return canvas;
}
// ===== ダウンロード処理 =====
async function downloadShareCard() {
const btn = document.getElementById(CONFIG.buttonId);
if (btn) {
btn.textContent = '⏳ 生成中...';
btn.disabled = true;
}
try {
const canvas = await generateShareCard();
const pageTitle = getPageTitle();
canvas.toBlob((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = cosense-share-${pageTitle.replace(/[^\w\u3000-\u9fff]/g, '_')}.png;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
if (btn) {
btn.textContent = '✅ 完了!';
setTimeout(() => {
btn.textContent = '📤 Share Card';
btn.disabled = false;
}, 2000);
}
}, 'image/png');
} catch (err) {
console.error('Share card generation failed:', err);
if (btn) {
btn.textContent = '❌ エラー';
setTimeout(() => {
btn.textContent = '📤 Share Card';
btn.disabled = false;
}, 2000);
}
}
}
// ===== ボタン追加 =====
function addShareButton() {
// 既に追加されていたら何もしない
if (document.getElementById(CONFIG.buttonId)) return;
const btn = document.createElement('button');
btn.id = CONFIG.buttonId;
btn.textContent = '📤 Share Card';
btn.title = 'Instagramストーリー用シェアカードを生成';
// スタイル
Object.assign(btn.style, {
position: 'fixed',
bottom: '20px',
right: '20px',
zIndex: '9999',
padding: '12px 20px',
backgroundColor: '#4CAF50',
color: '#fff',
border: 'none',
borderRadius: '28px',
fontSize: '14px',
fontWeight: 'bold',
cursor: 'pointer',
boxShadow: '0 4px 16px rgba(76, 175, 80, 0.4)',
transition: 'all 0.2s ease',
fontFamily: '"Noto Sans JP", sans-serif',
});
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'scale(1.05)';
btn.style.boxShadow = '0 6px 20px rgba(76, 175, 80, 0.6)';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = 'scale(1)';
btn.style.boxShadow = '0 4px 16px rgba(76, 175, 80, 0.4)';
});
btn.addEventListener('click', downloadShareCard);
document.body.appendChild(btn);
}
// ===== 初期化 =====
// ページ遷移を監視(Cosense SPAのため)
function init() {
// ページ表示時にボタン追加
addShareButton();
// SPAの画面遷移を監視
const observer = new MutationObserver(() => {
addShareButton();
});
observer.observe(document.body, { childList: true, subtree: true });
}
// DOMContentLoadedまたは即時実行
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
code:script.js
setTimeout(() => {
// チェックボックスとして使用する文字セットのリスト
const checkboxSetList = [
'⬜', '☑️',
'1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣',
'😀', '😐', '😧', '😨'
];
/*
'⬜', '✅'
'🔲', '☑️'
*/
const allBoxes = checkboxSetList.reduce((accu, current) => accu.concat(current), []);
const startsWithBoxReg = new RegExp('^\\s*(' + allBoxes.join('|') + ')');
const targetProject = scrapbox.Project.name;
class KeydownEvent {
constructor() {
this.textArea = document.getElementById('text-input');
this.event = document.createEvent('UIEvent');
this.event.initEvent('keydown', true, true);
}
dispatch(keyCode, withShift = false, withCtrl = false, withAlt = false, withCommand = false) {
this.event.keyCode = keyCode;
this.event.shiftKey = withShift;
this.event.ctrlKey = withCtrl;
this.event.altKey = withAlt;
this.event.metaKey = withCommand;
this.textArea.dispatchEvent(this.event);
}
}
// ボックスクリックでオンオフする
$('#app-container').off(click.toggleCheckBox_${targetProject}, '.lines');
$('#app-container').on(click.toggleCheckBox_${targetProject}, '.lines', async event => {
if (scrapbox.Project.name !== targetProject) {
$('#app-container').off(click.toggleCheckBox_${targetProject}, '.lines');
return;
}
const target = event.target;
if (!isFirstElementChild(target)||!isCharSpan(target, allBoxes)) return;
await new Promise(resolve => setTimeout(resolve, 30));
let lineString;
try {
lineString = getCursorLineString();
} catch (err) {
console.log(err);
return;
}
if (!startsWithBoxReg.test(lineString)) return;
const targetX = target.getBoundingClientRect().left;
const cursorX = document.getElementsByClassName('cursor')0.getBoundingClientRect().left;
const keydownEvent = new KeydownEvent();
if (cursorX <= targetX) {
keydownEvent.dispatch(39); // →
}
keydownEvent.dispatch(8); // Backspace
const newBox = (() => {
const trimmedLineString = lineString.trim();
for (const checkboxSet of checkboxSetList) {
for (let i = 0; i < checkboxSet.length; i++) {
if (trimmedLineString.startsWith(checkboxSeti)) {
return checkboxSeti + 1 < checkboxSet.length ? i + 1 : 0;
}
}
}
return target.textContent;
})();
writeText(newBox);
// この下のコメントアウトを解除すると、checked時に取消線を入れて時刻を追記します
// Mac、Porterでのみ動作します
/*
if (/Mobile/.test(navigator.userAgent)) return;
const targetBoxSet = checkboxSetList0;
if (!targetBoxSet.includes(newBox) || newBox === targetBoxSet0) return;
await new Promise(resolve => setTimeout(resolve, 30));
keydownEvent.dispatch(39, true, false, false, true); // shift + command + →
writeText('-');
keydownEvent.dispatch(39, false, false, false, true); // command + →
const now = moment().format('HH:mm');
writeText( ${now});
*/
});
// ボックス行で改行すると次行にボックス自動挿入
$('#text-input').off(keydown.autoInsertCheckBox_${targetProject});
$('#text-input').on(keydown.autoInsertCheckBox_${targetProject}, async event => {
if (scrapbox.Project.name !== targetProject) {
$('#text-input').off(keydown.autoInsertCheckBox_${targetProject});
return;
}
switch (event.key) {
case 'Enter': {
let currentLineString;
try {
currentLineString = getCursorLineString();
} catch (err) {
console.log(err);
return;
}
if (!startsWithBoxReg.test(currentLineString)) return;
await new Promise(resolve => setTimeout(resolve, 30));
let nextLineString;
try {
nextLineString = getCursorLineString();
} catch (err) {
console.log(err);
return;
}
if (!startsWithBoxReg.test(nextLineString)) {
const trimmedLineString = currentLineString.trim();
const targetBoxSet = checkboxSetList.find(boxSet => {
return boxSet.some(box => trimmedLineString.startsWith(box));
});
writeText(targetBoxSet0);
}
return;
}
default: {
return;
}
}
});
function isFirstElementChild(element) {
return element.parentNode.firstElementChild === element;
}
function getCursorLineString() {
return document.querySelector('.lines div.line.cursor-line').textContent;
}
function isCharSpan(element, targetCharList) {
return element.tagName === 'SPAN'
&& targetCharList.includes(element.textContent)
&& element.classList.value.split(' ').some(value => /^c\-\d+$/.test(value));
}
function writeText(text) {
const textArea = document.getElementById('text-input');
textArea.value = text;
textArea.dispatchEvent(new InputEvent('input', {bubbles: true, cancelable: true}));
}
}, 1500);
// Cosense TOC Sidebar
// 見出し (* , ** , *** 等) を右サイドに目次として常時表示
// コンテンツエリアの右端に追従 / PC (768px以上) のみ / テーマ自動対応
//
// 導入方法:
// 1. Cosenseプロジェクトの settings ページを開く
// 2. code:script.js ブロックを作成(または既存ブロックに追記)
// 3. このコードを貼り付けて保存 → リロード
(function () {
'use strict';
var STYLE = [
'#toc-sidebar {',
' position: fixed;',
' top: 48px;',
' width: 180px;',
' max-height: calc(100vh - 64px);',
' overflow-y: auto;',
' padding: 12px 0;',
' font-size: 12px;',
' z-index: 100;',
' scrollbar-width: thin;',
'}',
'#toc-sidebar::-webkit-scrollbar { width: 4px; }',
'#toc-sidebar::-webkit-scrollbar-thumb {',
' background: rgba(128,128,128,0.3);',
' border-radius: 2px;',
'}',
'#toc-sidebar .toc-label {',
' font-size: 10px;',
' font-weight: 600;',
' color: #35ac7c;',
' letter-spacing: 0.08em;',
' margin-bottom: 6px;',
' padding-left: 10px;',
'}',
'#toc-sidebar a {',
' display: block;',
' padding: 3px 8px;',
' color: #35ac7c;',
' font-weight: 600;',
' text-decoration: none;',
' border-left: 2px solid #35ac7c;',
' line-height: 1.5;',
' white-space: nowrap;',
' overflow: hidden;',
' text-overflow: ellipsis;',
'}',
'#toc-sidebar a:hover {',
' opacity: 0.7;',
'}',
'#toc-sidebar a.toc-sub {',
' padding-left: 18px;',
' font-size: 11px;',
'}',
'@media (max-width: 767px) {',
' #toc-sidebar { display: none !important; }',
'}',
].join('\n');
var styleEl = document.createElement('style');
styleEl.textContent = STYLE;
document.head.appendChild(styleEl);
var sidebar = document.createElement('nav');
sidebar.id = 'toc-sidebar';
document.body.appendChild(sidebar);
var items = [];
var debounceTimer = null;
var rafId = null;
function positionSidebar() {
var pageMenu = document.querySelector('#page-info-menu');
var editor = document.querySelector('.editor');
var left;
if (pageMenu) {
left = pageMenu.getBoundingClientRect().right + 12;
} else if (editor) {
left = editor.getBoundingClientRect().right + 48;
} else {
return;
}
var spaceRight = window.innerWidth - left;
if (spaceRight < 100) {
sidebar.style.display = 'none';
return;
}
sidebar.style.left = left + 'px';
sidebar.style.width = Math.min(180, spaceRight - 8) + 'px';
if (items.length > 0) sidebar.style.display = '';
}
function getHeadingLevel(line) {
for (var lv = 5; lv >= 1; lv--) {
if (line.querySelector('strong.level-' + lv)) return lv;
}
return 0;
}
function buildTOC() {
sidebar.innerHTML = '<div class="toc-label">目次</div>';
items = [];
var lines = document.querySelectorAll('.line');
var maxLevel = 0;
var entries = [];
for (var i = 0; i < lines.length; i++) {
var lv = getHeadingLevel(linesi);
if (lv === 0) continue;
var heading = linesi.querySelector('strongclass*="level"');
var text = heading.textContent.trim();
if (!text) continue;
if (lv > maxLevel) maxLevel = lv;
entries.push({ line: linesi, level: lv, text: text });
}
for (var j = 0; j < entries.length; j++) {
var entry = entriesj;
var a = document.createElement('a');
a.href = '#' + entry.line.id;
a.textContent = entry.text;
if (entry.level < maxLevel) a.className = 'toc-sub';
(function (lineEl) {
a.addEventListener('click', function (e) {
e.preventDefault();
lineEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
history.replaceState(null, '', '#' + lineEl.id);
});
})(entry.line);
sidebar.appendChild(a);
items.push({ el: a, line: entry.line });
}
sidebar.style.display = items.length > 0 ? '' : 'none';
positionSidebar();
updateActiveHeading();
}
function updateActiveHeading() {
if (items.length === 0) return;
var active = items0;
for (var i = 0; i < items.length; i++) {
var rect = itemsi.line.getBoundingClientRect();
if (rect.top <= 120) {
active = itemsi;
} else {
break;
}
}
for (var k = 0; k < items.length; k++) {
itemsk.el.classList.remove('toc-active');
}
if (active) active.el.classList.add('toc-active');
}
function scheduleBuild() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(buildTOC, 400);
}
function onScroll() {
if (rafId) return;
rafId = requestAnimationFrame(function () {
updateActiveHeading();
rafId = null;
});
}
var resizeTimer = null;
function onResize() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(positionSidebar, 100);
}
function init() {
var linesContainer = document.querySelector('.lines');
if (!linesContainer) {
setTimeout(init, 500);
return;
}
buildTOC();
var observer = new MutationObserver(scheduleBuild);
observer.observe(linesContainer, { childList: true, subtree: true });
window.addEventListener('scroll', onScroll, { passive: true });
window.addEventListener('resize', onResize, { passive: true });
}
var api = window.cosense || window.scrapbox;
if (api && api.on) {
api.on('page:changed', function () {
setTimeout(init, 300);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
// ==================================================
// Cosense UserScript: Markdown → Cosense記法 自動変換
// ==================================================
// 設置方法:
// 1. Cosenseプロジェクト内に新しいページを作成(例: md-to-cosense-converter)
// 2. ページ内に code:script.js のコードブロックを作成
// 3. このスクリプトの内容を貼り付け
// 4. プロジェクト設定ページの import に上記ページをリンク
// 例: /settings ページに /md-to-cosense-converter を追加
//
// 動作:
// 40文字以上のMarkdownテキストを貼り付けると、自動的にCosense記法に
// 変換してペーストされる。元に戻したい場合は Cmd+Z。
// ==================================================
(function () {
'use strict';
const MIN_LENGTH = 40;
// ============================================================
// Markdown 検出(2つ以上のパターンに一致で発動)
// ============================================================
function isMarkdown(text) {
const patterns = [
/^#{1,6}\s/m,
/\*\*^*+\*\*/,
/\.+?\\(.+?\)/,
/^\t *-*+\s/m,
/^`/m,
/^\d+\.\s/m,
/^>\s/m,
/!\.*?\\(.*?\)/,
/\|.+\|.+\|/,
/~~.+~~/,
];
let hits = 0;
for (const p of patterns) {
if (p.test(text)) hits++;
}
return hits >= 2;
}
// ============================================================
// インライン変換
// ============================================================
function convertInline(text) {
text = text.replace(/!\[(^\]*)\]\((^)+)\)/g, '$2');
text = text.replace(/\[(^\]+)\]\((^)+)\)/g, '$2 $1');
text = text.replace(/\*{3}(^*+)\*{3}/g, '$1');
text = text.replace(/\*{2}(^*+)\*{2}/g, '$1');
text = text.replace(/(?<!\*)\*(^*\n+)\*(?!\*)/g, '$1');
text = text.replace(/(?<!_)_(^_\n+)_(?!_)/g, '$1');
text = text.replace(/~~(^~+)~~/g, '$1');
text = text.replace(/(?<!\$)\$(?!\$)(^$\n+)\$(?!\$)/g, '$ $1');
return text;
}
// ==================================================
// Cosense UserScript: Markdown → Cosense記法 自動変換
// ==================================================
// 設置方法:
// 1. Cosenseプロジェクト内に新しいページを作成(例: md-to-cosense-converter)
// 2. ページ内に code:script.js のコードブロックを作成
// 3. このスクリプトの内容を貼り付け
// 4. プロジェクト設定ページの import に上記ページをリンク
// 例: /settings ページに /md-to-cosense-converter を追加
//
// 動作:
// 40文字以上のMarkdownテキストを貼り付けると、自動的にCosense記法に
// 変換してペーストされる。元に戻したい場合は Cmd+Z。
// ==================================================
(function () {
'use strict';
const MIN_LENGTH = 40;
// ============================================================
// Markdown 検出(2つ以上のパターンに一致で発動)
// ============================================================
function isMarkdown(text) {
const patterns = [
/^#{1,6}\s/m,
/\*\*^*+\*\*/,
/\.+?\\(.+?\)/,
/^\t *-*+\s/m,
/^`/m,
/^\d+\.\s/m,
/^>\s/m,
/!\.*?\\(.*?\)/,
/\|.+\|.+\|/,
/~~.+~~/,
];
let hits = 0;
for (const p of patterns) {
if (p.test(text)) hits++;
}
return hits >= 2;
}
// ============================================================
// インライン変換
// ============================================================
function convertInline(text) {
text = text.replace(/!\[(^\]*)\]\((^)+)\)/g, '$2');
text = text.replace(/\[(^\]+)\]\((^)+)\)/g, '$2 $1');
text = text.replace(/\*{3}(^*+)\*{3}/g, '$1');
text = text.replace(/\*{2}(^*+)\*{2}/g, '$1');
text = text.replace(/(?<!\*)\*(^*\n+)\*(?!\*)/g, '$1');
text = text.replace(/(?<!_)_(^_\n+)_(?!_)/g, '$1');
text = text.replace(/~~(^~+)~~/g, '$1');
text = text.replace(/(?<!\$)\$(?!\$)(^$\n+)\$(?!\$)/g, '$ $1');
return text;
}
// ============================================================
// Markdown → Cosense記法 変換
// ============================================================
function convertMarkdownToCosense(md) {
const lines = md.split('\n');
const result = [];
let inCodeBlock = false;
let tableStarted = false;
for (let i = 0; i < lines.length; i++) {
const line = linesi;
const codeFence = line.match(/^(`{3,})(\w*)(\s*)$/);
if (codeFence) {
if (!inCodeBlock) {
inCodeBlock = true;
const lang = codeFence2 || 'txt';
result.push(code:snippet.${lang});
} else {
inCodeBlock = false;
}
continue;
}
if (inCodeBlock) {
result.push( ${line});
continue;
}
if (/^\|(.+)\|\s*$/.test(line)) {
if (/^\|\s:|\-+\|\s*$/.test(line)) continue;
if (!tableStarted) {
tableStarted = true;
result.push('table:table');
}
const cells = line
.replace(/^\|/, '')
.replace(/\|\s*$/, '')
.split('|')
.map(c => convertInline(c.trim()));
result.push( ${cells.join('\t')});
continue;
}
if (tableStarted) tableStarted = false;
if (/^\$\$\s*$/.test(line)) {
const mathLines = [];
let j = i + 1;
while (j < lines.length && !/^\$\$\s*$/.test(linesj)) {
mathLines.push(linesj);
j++;
}
if (j < lines.length) {
result.push([$ ${mathLines.join(' ')}]);
i = j;
continue;
}
}
const hMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (hMatch) {
const level = hMatch1.length;
const text = convertInline(hMatch2);
const stars = Math.max(1, 5 - level);
result.push([${'*'.repeat(stars)} ${text}]);
continue;
}
if (/^-*_{3,}\s*$/.test(line)) {
result.push('/hr');
continue;
}
const qMatch = line.match(/^>\s?(.*)$/);
if (qMatch) {
result.push(> ${convertInline(qMatch[1])});
continue;
}
const ulMatch = line.match(/^(\s*)-*+\s+(.+)$/);
if (ulMatch) {
const origIndent = ulMatch1.length;
const depth = Math.floor(origIndent / 2) + 1;
let content = convertInline(ulMatch2);
const cb = content.match(/^\[( xX)\]\s*(.+)$/);
if (cb) {
const mark = cb1.toLowerCase() === 'x' ? 'x' : ' ';
result.push(${' '.repeat(depth)}${mark} ${cb[2]});
} else {
result.push(${' '.repeat(depth)}${content});
}
continue;
}
const olMatch = line.match(/^(\s*)\d+\.\s+(.+)$/);
if (olMatch) {
const origIndent = olMatch1.length;
const depth = Math.floor(origIndent / 2) + 1;
result.push(${' '.repeat(depth)}${convertInline(olMatch[2])});
continue;
}
result.push(convertInline(line));
}
return result.join('\n');
}
// ============================================================
// トースト通知
// ============================================================
function showToast(msg) {
const existing = document.getElementById('md-convert-toast');
if (existing) existing.remove();
const toast = document.createElement('div');
toast.id = 'md-convert-toast';
toast.textContent = msg;
toast.style.cssText = `
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 10px 20px;
border-radius: 8px;
font-size: 13px;
font-family: -apple-system, BlinkMacSystemFont, 'Hiragino Sans', sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
z-index: 100001;
pointer-events: none;
`;
document.body.appendChild(toast);
setTimeout(() => {
toast.style.transition = 'opacity 0.3s';
toast.style.opacity = '0';
setTimeout(() => toast.remove(), 300);
}, 2000);
}
// ============================================================
// ペーストイベント
// ============================================================
function handlePaste(e) {
const cd = e.clipboardData || window.clipboardData;
if (!cd) return;
const text = cd.getData('text/plain');
if (!text || text.length < MIN_LENGTH) return;
if (!isMarkdown(text)) return;
const converted = convertMarkdownToCosense(text);
// 元のペーストを止める
e.preventDefault();
e.stopPropagation();
// Scrapboxの隠しtextarea経由でペースト
const textInput = document.getElementById('text-input');
if (!textInput) return;
textInput.focus();
textInput.value = converted;
textInput.dispatchEvent(new InputEvent('input', {
bubbles: true,
cancelable: false,
inputType: 'insertFromPaste',
data: converted,
}));
showToast('Cosense記法に変換しました(Cmd+Z で取消)');
}
// ============================================================
// 初期化
// ============================================================
function init() {
const editor = document.querySelector('.editor');
if (editor) {
editor.addEventListener('paste', handlePaste, true);
console.log('MD→Cosense Auto converter initialized');
} else {
setTimeout(init, 1000);
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();