togamin-settings
from togaminnnn.icon
QuickQuotes
ctrl + command + uでコピー
code:script.js
var onKeyDown = function(e){
var name = e.code;
if (e.metaKey) {
if (e.shiftKey){
if(name == "KeyU"){
if(document.getElementsByClassName("cursor-line")0){ var quote=window.getSelection().toString();
if(window.getSelection().toString()==""){
var url = location.href + "#" +
document.getElementsByClassName("cursor-line")0.id.slice(1); }else{
var lines = quote.split(/\n/g).map(function(line){return ' > '+line})
var url = "+location.href.split("/")[3 +
"/" + scrapbox.Page.title +
"#"+ document.getElementsByClassName("cursor-line")0.id.slice(1) + "]" +
"\r" +lines.join('\n');
}
}else{
var url = location.href
}
var listener = function(e){
e.clipboardData.setData("text/plain" , url);
// 本来のイベントをキャンセル
e.preventDefault();
// 終わったら一応削除
document.removeEventListener("copy", listener);
}
document.addEventListener("copy" , listener);
document.execCommand("copy");
}
}
}
}
document.addEventListener('keydown', onKeyDown);
Tweet Menu
code:script.js
scrapbox.PageMenu.addItem({
title: 'Tweet',
onClick: () => window.open(https://twitter.com/intent/tweet?url=${encodeURIComponent(location.href)}&text=${encodeURIComponent(window.scrapbox.Page.title)})
})
Mermaid記法を可視化する
code:script.js
$(() => {
.done((script, textStatus) => {
mermaid.mermaidAPI.initialize({
startOnLoad: false
})
const mermaidViewer = new MermaidViewer()
mermaidViewer.onScrapboxPageChanged()
scrapbox.on("page:changed", () => mermaidViewer.onScrapboxPageChanged())
scrapbox.on("lines:changed", () => mermaidViewer.onScrapboxLinesChanged())
})
.fail((jqxhr, settings, exception) => {
console.error(exception)
})
const MermaidViewer = function () {
const DEFAULT_SHOW_CODE = false
this.recentMermaidCodes = new Map()
this.codeViewStatusRepository = new MermaidCodeViewStatusRepository()
this.onScrapboxLinesChanged = function () {
if (scrapbox.Page.lines) {
this.updateDiagrams()
}
}
this.onScrapboxPageChanged = function () {
if (scrapbox.Page.lines) {
this.updateDiagrams()
this.setAllCodeViewStatus(DEFAULT_SHOW_CODE)
}
}
// すべてのコードブロックの表示ステータスを変更
// 引数: value 表示ステータス (true|false)
this.setAllCodeViewStatus = function (value) {
for (const id, code of this.recentMermaidCodes) { code.setCodeViewStatus(value)
}
}
// 変更があればダイアグラムを更新
this.updateDiagrams = function () {
const newCodes = this.findMermaidCodes()
const diff = MermaidViewerUtils.diffMermaidCodes(this.recentMermaidCodes, newCodes)
for (const item of diff) {
if (item.op === "delete") {
item.code.deleteDiagram()
} else {
item.code.updateDiagram()
}
}
this.recentMermaidCodes = newCodes
}
// mermaidコードをページ内から検索
// 戻り値: Map型
// キー: コードブロックのID(最初の行ID)
// 値: MermaidCode
this.findMermaidCodes = function () {
const result = new Map()
var text, filename, id, lastLineId, lineIds
for (const line of scrapbox.Page.lines) {
if (line.codeBlock && line.codeBlock.lang === "mermaid") {
if (line.codeBlock.start) {
text = ""
id = line.id
lineIds = new Set()
} else {
text += "\n" + line.text
}
lineIds.add(line.id)
if (line.codeBlock.end) {
lastLineId = line.id
text = text.trim()
result.set(id, new MermaidCode(id, text, lastLineId, lineIds, this.codeViewStatusRepository))
}
}
}
return result
}
}
const MermaidCode = function (id, text, lastLineId, lineIds, codeViewStatusRepository) {
const MERMAID_SVG_ID_PREFIX = "mermaid-"
this.id = id
this.text = text
this.lastLineId = lastLineId
this.lineIds = lineIds
this.codeViewStatusRepository = codeViewStatusRepository
this.svgId = MERMAID_SVG_ID_PREFIX + id
// mermaidダイアグラムを更新
this.updateDiagram = function () {
try {
const svg = mermaid.mermaidAPI.render(this.svgId, this.text)
$("#" + this.svgId).remove()
$("#L" + this.lastLineId).after(svg)
} catch (e) {
console.error(e)
$("#L" + this.lastLineId).after($("#" + this.svgId))
}
$("#" + this.svgId)
.on("click", () => this.onSvgClicked())
.css("cursor", "pointer")
}
// mermaidダイアグラムを削除
this.deleteDiagram = function () {
$("#" + this.svgId).remove()
}
// mermaidダイアグラム(SVG)がクリックされたときのイベントハンドラ
// コードブロックの表示ステータスを変更
this.onSvgClicked = function () {
this.codeViewStatusRepository.changeStatus(this.id)
this.applyCodeView()
}
// コードブロックの表示ステータスを適用
this.applyCodeView = function () {
const status = this.codeViewStatusRepository.getStatus(this.id)
for (const lineId of this.lineIds) {
if (status) {
$("#L" + lineId).show(100)
} else {
$("#L" + lineId).hide(100)
}
}
}
// コードブロックの表示ステータスを変更
this.setCodeViewStatus = function (value) {
this.codeViewStatusRepository.setStatus(this.id, value)
this.applyCodeView()
}
}
const MermaidCodeViewStatusRepository = function () {
this.status = new Map()
this.defaultValue = true
this.changeStatus = function (id) {
const old = this.status.has(id) ? this.status.get(id) : this.defaultValue
this.status.set(id, !old)
}
this.getStatus = function (id) {
return this.status.has(id) ? this.status.get(id) : this.defaultValue
}
this.setStatus = function (id, value) {
this.status.set(id, value)
}
}
const MermaidViewerUtils = {}
// 2つのMap型に格納されたコードの差分を返す
// 引数: oldMap 古い値(Map型)
// 引数: newMap 新しい値(Map型)
MermaidViewerUtils.diffMermaidCodes = function (oldMap, newMap) {
const result = []
const intersection = new Set()
if (!oldMap.has(key)) {
result.push({
op: "new",
key: key,
code: newMap.get(key)
})
} else {
intersection.add(key)
}
}
if (!newMap.has(key)) {
result.push({
op: "delete",
key: key,
code: oldMap.get(key)
})
intersection.delete(key)
}
}
for (const key of intersection) {
const oldVal = oldMap.get(key)
const newVal = newMap.get(key)
if (oldVal.text !== newVal.text) {
result.push({
op: "changed",
key: key,
code: newMap.get(key)
})
}
}
return result;
}
})
強調記法からページの見出しを作るUserScript
code:script.js
scrapbox.PageMenu.addMenu({
title: '見出し',
onClick: () => {
scrapbox.PageMenu('見出し').removeAllItems()
for (let line of scrapbox.Page.lines) {
if (!line.section.start) continue
if (!line.text.startsWith("[* ") && !line.text.startsWith("[** ") && !line.text.startsWith("[*** ")) continue
if (!line.nodes) continue
const image = ""
const noIcon = false
// hogeとhogeは先頭をインデントする。
const marginLeft = line.text.startsWith("[* ") ? " ・" : ""
const marginLeft2 = line.text.startsWith("[** ") ? "●" : ""
const title = marginLeft + marginLeft2 + renderPlainText(line.nodes)
const onClick = () => location.hash = line.id
scrapbox.PageMenu('見出し').addItem({ title, image, onClick })
}
}
})
function renderPlainText(node) {
if (node instanceof Array) return node.map(node => renderPlainText(node)).join('')
if (typeof node === 'string') return node
switch (node.type) {
case 'icon':
case 'strong-icon':
return node.unit.page
}
return renderPlainText(node.children)
}
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
}
足跡の表示
code:script.js
(function (history) {
// 表示する履歴の数
const maxHistory = 7;
if ($('#history-box').length) { return; }
const $header = $(".quick-launch");
$header.append($(`<div class='flex-box'>
<div class='flex-item'>
<div class='left-box' id='history-box' />
</div></div>`));
const $historyHolder = $("#history-box");
function pathToPageTitle(path) {
return decodeURIComponent(path.split("/")2); }
let pushState = history.pushState;
history.pushState = function (state) {
try {
// on history change
var pageTitle = pathToPageTitle(state.path);
if (pageTitle) {
let $newHistory = $(<span>→ <a href='${state.path}'>${pageTitle}</a></span>);
$newHistory.on("click", () => { $newHistory.remove(); });
$historyHolder.append($newHistory);
if ($historyHolder.children().length > maxHistory) {
$historyHolder.children().first().remove();
}
}
} catch (x) {
console.error(x);
}
return pushState.apply(history, arguments);
};
})(window.history);
チェックボックス
code:script.js
setTimeout(() => {
// チェックボックスとして使用する文字セットのリスト
const checkboxSetList = [
];
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 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);