https://let.hatelabo.jp/rss
Recent Bookmarklets
-
https://let.hatelabo.jp/Lhankor_Mhy/let/jtaB0fa4gYAA
2024-03-28T03:21:28Z
コメントメタブと2階ブクマを展開します。説明→https://realtor-readyabooks.hatenablog.com/entry/2022/12/28/153626
Lhankor_Mhy
[Let] はてなブックマークメタブ展開
<a href="javascript:%28async%28%29%3D%3E%7Bconst%20APIURL%3D%27https%3A%2F%2Fb.hatena.ne.jp%2Fentry%2Fjsonlite%2F%3Furl%3D%27%3Bdocument.body.insertAdjacentHTML%28%27beforeend%27%2C%60%5Cn%3Cstyle%3E%5Cn%5Bdata-metabu%5D%20%7B%5Cn%5Ctbackground-color%3A%20rgba%280%2C0%2C0%2C0.05%29%3B%5Cn%7D%5Cn%5Bdata-metabu%5D.focus%20%7B%5Cn%5Ctoutline%3Adotted%201px%3B%5Cn%7D%5Cn%23metahatebu-popover%7B%5Cn%20%20%20%20inset%3A%20auto%200%200%20auto%3B%5Cn%20%20%20%20opacity%3A%200.5%3B%5Cn%20%20%20%20border%3A%20none%3B%5Cn%20%20%20%20background-color%3A%20%23000%3B%5Cn%20%20%20%20color%3A%20%23fff%3B%5Cn%20%20%20%20position%3A%20fixed%3B%5Cn%7D%5Cn%3C%2Fstyle%3E%5Cn%3Cdialog%20popover%20id%3D%22metahatebu-popover%22%3E%E3%83%A1%E3%82%BF%E3%83%96%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99%3Cbutton%20data-metabuprev%3E%E5%89%8D%3C%2Fbutton%3E%3Cbutton%20data-metabunext%3E%E6%AC%A1%3C%2Fbutton%3E%3C%2Fdialog%3E%5Cn%20%20%20%20%60%29%3Bdocument.querySelector%28%27.entry-comments%27%29.insertAdjacentHTML%28%27beforeend%27%2C%60%5Cn%3Cdiv%20class%3D%22entry-comment-contents%22%20id%3D%22upStairsSeat%22%3E%3C%2Fdiv%3E%5Cn%20%20%20%20%60%29%3Bconst%20metahatebuPopover%3Ddocument.getElementById%28%27metahatebu-popover%27%29%3BmetahatebuPopover.addEventListener%28%27click%27%2C%28%28i%3D-1%29%3D%3Eevent%3D%3E%7Bconst%20metabues%3Ddocument.querySelectorAll%28%27%5Bdata-metabu%5D%27%29%3Bif%28event.target.matches%28%27%5Bdata-metabuprev%5D%27%29%29i--%3Bif%28event.target.matches%28%27%5Bdata-metabunext%5D%27%29%29i%2B%2B%3Bi%3Dmetabues.length%3F%28i%2Bmetabues.length%29%25metabues.length%3A0%3Bmetabues%5Bi%5D.scrollIntoView%28%7Bbehavior%3A%22smooth%22%2Cblock%3A%22end%22%2Cinline%3A%22nearest%22%7D%29%3Bmetabues.forEach%28%28%28metabu%2Cj%29%3D%3Emetabu.classList.toggle%28%27focus%27%2Ci%3D%3Dj%29%29%29%7D%29%28%29%29%3Bconst%20metahatebuPopoverOpen%3D%28%29%3D%3E%7Bif%28HTMLElement.showPopover%29%7BmetahatebuPopover.showPopover%28%29%7Delse%7BmetahatebuPopover.show%28%29%7D%7D%3Bconst%20addBookmark%3Dfunction%28targetElement%2Cbookmark%2Ceid%29%7Bdocument.querySelector%28%60%5Bhref%3D%22%2Fentry%2F%24%7Beid%7D%2Fcomment%2F%24%7Bbookmark.user%7D%22%5D%60%29%3F.closest%28%27.entry-comment-contents%20%3E%20.entry-comment-contents%27%29.remove%28%29%3BtargetElement.insertAdjacentHTML%28%27beforeend%27%2Cdocument.getElementById%28%27autoloader-bookmark-item%27%29.textContent.replaceAll%28%27%3Cdiv%20class%3D%22entry-comment-contents%20%27%2C%27%3Cdiv%20data-metabu%3D%22metabu%22%20class%3D%22entry-comment-contents%20%27%29.replaceAll%28%27%7B%7Buser_name%7D%7D%27%2Cbookmark.user%29.replaceAll%28%27%7B%7Bbookmarked_url%7D%7D%27%2Cbookmark.url%29.replaceAll%28%27%7B%7Buser_page_path%7D%7D%27%2C%60%2F%24%7Bbookmark.user%7D%2F%60%29.replaceAll%28%27%7B%7B%20sort%20%7D%7D%27%2C%60recent%60%29.replaceAll%28%27%7B%7Bprofile_image_url%7D%7D%27%2C%60https%3A%2F%2Fcdn.profile-image.st-hatena.com%2Fusers%2F%24%7Bbookmark.user%7D%2Fprofile.png%60%29.replaceAll%28%27%7B%7B%20%23is_public%20%7D%7Dis-hidden%7B%7B%20%2Fis_public%20%7D%7D%27%2C%60is-hidden%60%29.replaceAll%28%27%7B%7B%7Bcomment_expanded%7D%7D%7D%27%2Cbookmark.comment%29.replaceAll%28%27%7B%7B%7Btags%7D%7D%7D%27%2C%60%3Cli%3E%24%7Bbookmark.tags.join%28%27%3Cli%3E%27%29%7D%60%29.replaceAll%28%27%7B%7Bcreated%7D%7D%27%2Cbookmark.timestamp%29.replaceAll%28%27%7B%7Bcomment_page_path%7D%7D%27%2C%60%2Fentry%2F%24%7Beid%7D%2Fcomment%2F%24%7Bbookmark.user%7D%60%29.replaceAll%28%27%7B%7B%23should_nofollow%7D%7Dnofollow%7B%7B%2Fshould_nofollow%7D%7D%27%2C%27nofollow%27%29.replaceAll%28%27%7B%7B%23enable_button%7D%7D%20is-enabled%7B%7B%2Fenable_button%7D%7D%27%2C%27is-enabled%27%29%29%3BmetahatebuPopoverOpen%28%29%7D%3Bconst%20targetURLs%3D%5Blocation.href%5D%2CupstairsBookmarksList%3D%5B%5D%3Bfor%28const%20targetURL%20of%20targetURLs%29%7Bconst%7Bbookmarks%3Abookmarks%2Centry_url%3Aentry_url%2Ceid%3Aeid%7D%3Dawait%28await%20fetch%28%60%24%7BAPIURL%7D%24%7BencodeURIComponent%28targetURL%29%7D%60%29%29.json%28%29%3F%3F%7Bentry_url%3Anull%7D%3Bif%28%21entry_url%29continue%3BtargetURLs.push%28entry_url%29%3BupstairsBookmarksList.push%28%5Bbookmarks%2Ceid%5D%29%7Dfunction%20addUpstairsBookmark%28targetElements%29%7Bif%28upstairsBookmarksList.length%29metahatebuPopoverOpen%28%29%3BupstairsBookmarksList.forEach%28%28upstairsBookmarks%3D%3E%7Bconst%5Bbookmarks%2Ceid%5D%3DupstairsBookmarks%3BupstairsBookmarks%5B0%5D%3Dbookmarks.filter%28%28bookmark%3D%3E%7Bconst%20targetUserBookmark%3DtargetElements.find%28%28element%3D%3Eelement.dataset%3F.userName%3D%3D%3Dbookmark.user%29%29%3Bif%28%21targetUserBookmark%29%7Bconst%20targetElement%3Ddocument.getElementById%28%27upStairsSeat%27%29%3BaddBookmark%28targetElement%2Cbookmark%2Ceid%29%3Breturn%20true%7Dconst%20targetElement%3D%5BtargetUserBookmark%2C...targetUserBookmark.querySelectorAll%28%60.entry-comment-contents%5Bdata-user-name%3D%22%24%7Bbookmark.user%7D%22%5D%60%29%5D.pop%28%29%3BaddBookmark%28targetElement%2Cbookmark%2Ceid%29%3Breturn%20false%7D%29%29%7D%29%29%7Dfunction%20addMetaBookmark%28targetElements%29%7BtargetElements.forEach%28%28async%20element%3D%3E%7Bconst%20targetURL%3Delement.querySelector%28%27%5Bdata-gtm-label%3D%22entry-recent-permalink%22%5D%27%29%3F.href%3Bif%28%21targetURL%29return%20undefined%3Bconst%7Bbookmarks%3Abookmarks%2Centry_url%3Aentry_url%2Ceid%3Aeid%7D%3Dawait%28await%20fetch%28%60%24%7BAPIURL%7D%24%7BencodeURIComponent%28targetURL%29%7D%60%29%29.json%28%29%3F%3F%7Bentry_url%3Anull%7D%3Bif%28%21entry_url%29return%20undefined%3Bbookmarks.forEach%28%28bookmark%3D%3E%7BaddBookmark%28element%2Cbookmark%2Ceid%29%7D%29%29%7D%29%29%7DaddUpstairsBookmark%28Array.from%28document.querySelectorAll%28%60.js-bookmarks-recent%20%5Bdata-user-name%5D%60%29%29%29%3BaddMetaBookmark%28Array.from%28document.querySelectorAll%28%60.js-bookmarks-recent%20%5Bdata-user-name%5D%60%29%29%29%3Bconst%20targetNode%3Ddocument.querySelector%28%27.js-bookmarks-recent%27%29%3Bconst%20upstairsBookmarkMutationConfig%3D%7BchildList%3Atrue%7D%3Bconst%20upstairsBookmarkMutationCallback%3Dfunction%28mutationsList%2Cobserver%29%7Bfor%28const%20mutation%20of%20mutationsList%29%7Bif%28mutation.type%3D%3D%3D%27childList%27%29%7BaddUpstairsBookmark%28Array.from%28mutation.addedNodes%29.filter%28%28node%3D%3Enode.dataset%3F.userName%29%29%29%7D%7D%7D%3Bconst%20upstairsBookmarkMutationObserver%3Dnew%20MutationObserver%28upstairsBookmarkMutationCallback%29%3BupstairsBookmarkMutationObserver.observe%28targetNode%2CupstairsBookmarkMutationConfig%29%3Bconst%20metaBookmarkMutationConfig%3D%7BchildList%3Atrue%2Csubtree%3Atrue%7D%3Bconst%20metaBookmarkMutationCallback%3Dfunction%28mutationsList%2Cobserver%29%7Bfor%28const%20mutation%20of%20mutationsList%29%7Bif%28mutation.type%3D%3D%3D%27childList%27%29%7BaddMetaBookmark%28Array.from%28mutation.addedNodes%29.filter%28%28node%3D%3Enode.dataset%3F.userName%29%29%29%7D%7D%7D%3Bconst%20metaBookmarkMutationObserver%3Dnew%20MutationObserver%28metaBookmarkMutationCallback%29%3BmetaBookmarkMutationObserver.observe%28targetNode%2CmetaBookmarkMutationConfig%29%7D%29%28%29%3B">はてなブックマークメタブ展開</a><pre>// ==UserScript==
// @name はてなブックマークメタブ展開
// @title はてなブックマークメタブ展開
// @namespace https://let.hatelabo.jp/Lhankor_Mhy/let/jtaB0fa4gYAA
// @version 0.13.2
// @description コメントメタブと2階ブクマを展開します。説明→https://realtor-readyabooks.hatenablog.com/entry/2022/12/28/153626
// @author Lhankor_Mhy
// @match https://b.hatena.ne.jp/entry/*
// @icon https://b.hatena.ne.jp/favicon.ico
// @license CC0
// @noframes
// @grant none
// @javascript_url
// ==/UserScript==
(async () => {
const APIURL = 'https://b.hatena.ne.jp/entry/jsonlite/?url='
document.body.insertAdjacentHTML('beforeend', `
<style>
[data-metabu] {
background-color: rgba(0,0,0,0.05);
}
[data-metabu].focus {
outline:dotted 1px;
}
#metahatebu-popover{
inset: auto 0 0 auto;
opacity: 0.5;
border: none;
background-color: #000;
color: #fff;
position: fixed;
}
</style>
<dialog popover id="metahatebu-popover">メタブあります<button data-metabuprev>前</button><button data-metabunext>次</button></dialog>
`)
document.querySelector('.entry-comments').insertAdjacentHTML('beforeend', `
<div class="entry-comment-contents" id="upStairsSeat"></div>
`)
// メタブありますポップオーバーのイベント設定
const metahatebuPopover = document.getElementById('metahatebu-popover');
metahatebuPopover.addEventListener('click', ((i = -1) => event => {
const metabues = document.querySelectorAll('[data-metabu]');
if (event.target.matches('[data-metabuprev]')) i--;
if (event.target.matches('[data-metabunext]')) i++;
i = metabues.length ? (i + metabues.length) % metabues.length : 0;
metabues[i].scrollIntoView({ behavior: "smooth", block: "end", inline: "nearest" });
metabues.forEach((metabu, j) => metabu.classList.toggle('focus', i == j));
})())
// ポップオーバーフォールバック
const metahatebuPopoverOpen = () => {
if (HTMLElement.showPopover) {
metahatebuPopover.showPopover()
} else {
metahatebuPopover.show()
}
}
// ブクマ展開(x-template再利用)
const addBookmark = function (targetElement, bookmark, eid) {
document.querySelector(`[href="/entry/${eid}/comment/${bookmark.user}"]`)?.closest('.entry-comment-contents > .entry-comment-contents').remove();
targetElement.insertAdjacentHTML(
'beforeend',
document.getElementById('autoloader-bookmark-item').textContent
.replaceAll('<div class="entry-comment-contents ', '<div data-metabu="metabu" class="entry-comment-contents ')
.replaceAll('{{user_name}}', bookmark.user)
.replaceAll('{{bookmarked_url}}', bookmark.url)
.replaceAll('{{user_page_path}}', `/${bookmark.user}/`)
.replaceAll('{{ sort }}', `recent`)
.replaceAll('{{profile_image_url}}', `https://cdn.profile-image.st-hatena.com/users/${bookmark.user}/profile.png`)
.replaceAll('{{ #is_public }}is-hidden{{ /is_public }}', `is-hidden`)
.replaceAll('{{{comment_expanded}}}', bookmark.comment)
.replaceAll('{{{tags}}}', `<li>${bookmark.tags.join('<li>')}`)
.replaceAll('{{created}}', bookmark.timestamp)
.replaceAll('{{comment_page_path}}', `/entry/${eid}/comment/${bookmark.user}`)
.replaceAll('{{#should_nofollow}}nofollow{{/should_nofollow}}', 'nofollow')
.replaceAll('{{#enable_button}} is-enabled{{/enable_button}}', 'is-enabled')
)
metahatebuPopoverOpen()
}
// 2階ブクマAPIコール
const targetURLs = [location.href], upstairsBookmarksList = []
for (const targetURL of targetURLs) {
const { bookmarks, entry_url, eid } = (await (await fetch(`${APIURL}${encodeURIComponent(targetURL)}`)).json()) ?? { entry_url: null }
if (!entry_url) continue
targetURLs.push(entry_url)
upstairsBookmarksList.push([bookmarks, eid])
}
// 2階ブクマをHTMLに展開
function addUpstairsBookmark(targetElements) {
if (upstairsBookmarksList.length) metahatebuPopoverOpen()
upstairsBookmarksList.forEach(upstairsBookmarks => {
const [bookmarks, eid] = upstairsBookmarks;
upstairsBookmarks[0] = bookmarks.filter(bookmark => {
const targetUserBookmark = targetElements.find(element => element.dataset?.userName === bookmark.user)
if (!targetUserBookmark) {
const targetElement = document.getElementById('upStairsSeat');
addBookmark(targetElement, bookmark, eid);
return true // 遅延読み込み後に再処理するかもしれないのでリストに残す
}
const targetElement = [targetUserBookmark, ...targetUserBookmark.querySelectorAll(`.entry-comment-contents[data-user-name="${bookmark.user}"]`)].pop()
addBookmark(targetElement, bookmark, eid)
return false // 遅延読み込み後に再処理しないのでリストから削除する
})
})
}
// メタブをHTMLに展開
function addMetaBookmark(targetElements) {
targetElements.forEach(async element => {
const targetURL = element.querySelector('[data-gtm-label="entry-recent-permalink"]')?.href
if (!targetURL) return undefined
const { bookmarks, entry_url, eid } = (await (await fetch(`${APIURL}${encodeURIComponent(targetURL)}`)).json()) ?? { entry_url: null }
if (!entry_url) return undefined
bookmarks.forEach(
bookmark => {
addBookmark(element, bookmark, eid)
}
)
})
}
// 初回呼び出し
addUpstairsBookmark(Array.from(document.querySelectorAll(`.js-bookmarks-recent [data-user-name]`)))
addMetaBookmark(Array.from(document.querySelectorAll(`.js-bookmarks-recent [data-user-name]`)))
// ブクマ遅延読み込み監視
const targetNode = document.querySelector('.js-bookmarks-recent');
const upstairsBookmarkMutationConfig = { childList: true };
const upstairsBookmarkMutationCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
addUpstairsBookmark(Array.from(mutation.addedNodes).filter(node => node.dataset?.userName))
}
}
};
const upstairsBookmarkMutationObserver = new MutationObserver(upstairsBookmarkMutationCallback);
upstairsBookmarkMutationObserver.observe(targetNode, upstairsBookmarkMutationConfig);
const metaBookmarkMutationConfig = { childList: true, subtree: true };
const metaBookmarkMutationCallback = function (mutationsList, observer) {
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
addMetaBookmark(Array.from(mutation.addedNodes).filter(node => node.dataset?.userName))
}
}
};
const metaBookmarkMutationObserver = new MutationObserver(metaBookmarkMutationCallback);
metaBookmarkMutationObserver.observe(targetNode, metaBookmarkMutationConfig);
})()
</pre>
-
https://let.hatelabo.jp/Lhankor_Mhy/let/kLOm3rHEgeAA
2024-03-12T03:15:07Z
Pocket のリンクから ?utm_source=pocket_saves などを外します。
Lhankor_Mhy
[Let] Pocket のトラッキングURLパラメータを外す
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FLhankor_Mhy%2Flet%2FkLOm3rHEgeAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">Pocket のトラッキングURLパラメータを外す</a><pre>// ==UserScript==
// @name Pocket のトラッキングURLパラメータを外す
// @title Pocket のトラッキングURLパラメータを外す
// @namespace https://let.hatelabo.jp/Lhankor_Mhy/let/kLOm3rHEgeAA
// @version 0.1.2
// @description Pocket のリンクから ?utm_source=pocket_saves などを外します。
// @author Lhankor_Mhy
// @license CC0
// @match https://getpocket.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=getpocket.com
// @run-at document-idle
// @grant none
// ==/UserScript==
{
let timerId;
new MutationObserver((mutationList, observer) => {
mutationList.forEach((mutation) => {
switch (mutation.type) {
case "childList":
clearTimeout(timerId);
timerId = setTimeout(omitTrackingParameter, 500)
break;
}
});
}).observe(document.body, {
childList: true,
subtree: true,
});
const omitTrackingParameter = () =>
document.querySelectorAll('a:where([data-testid="image-link"],[data-testid="content-block"],[data-testid="view-original"],[data-testid="publisher-link"])').forEach(el => {
const url = new URL(el.href);
const searchPrams = url.searchParams;
searchPrams.delete('utm_source');
url.search = searchPrams.toString();
el.href = url.href;
})
}</pre>
-
https://let.hatelabo.jp/onk/let/kO_0gsqOgeAA
2024-03-09T07:19:57Z
個別 Tweet の URL から前後 1 時間の投稿を RT を含めて表示する
onk
[Let] Twitter(現X)の前後ポストを表示する
<a href="javascript:%28%28%29%3D%3E%7Bconst%20TWITTER_EPOCH%3D1288834974657%3Bconst%20formatTwitterDate%3Ddate%3D%3E%7Bconst%20pad2%3Dn%3D%3En.toString%28%29.padStart%282%2C%270%27%29%3Bconst%20year%3Ddate.getFullYear%28%29%3Bconst%20month%3Dpad2%28date.getMonth%28%29%2B1%29%3Bconst%20day%3Dpad2%28date.getDate%28%29%29%3Bconst%20hour%3Dpad2%28date.getHours%28%29%29%3Bconst%20minute%3Dpad2%28date.getMinutes%28%29%29%3Bconst%20second%3Dpad2%28date.getSeconds%28%29%29%3Breturn%60%24%7Byear%7D-%24%7Bmonth%7D-%24%7Bday%7D_%24%7Bhour%7D%3A%24%7Bminute%7D%3A%24%7Bsecond%7D_JST%60%7D%3Bconst%20getSearchRange%3DsnowflakeStr%3D%3E%7Bconst%20snowflake%3DBigInt%28snowflakeStr%29%3Bconst%20timestamp%3DNumber%28snowflake%3E%3E22n%29%2BTWITTER_EPOCH%3Breturn%7Bsince%3AformatTwitterDate%28new%20Date%28timestamp-60%2A60%2A1e3%29%29%2Cuntil%3AformatTwitterDate%28new%20Date%28timestamp%2B60%2A60%2A1e3%29%29%7D%7D%3Bconst%20main%3D%28%29%3D%3E%7Bconst%20match%3Dlocation.href.match%28%2Fhttps%3A%5C%2F%5C%2Ftwitter%5C.com%5C%2F%28%3F%3Cname%3E%5B%5E%2F%5D%2B%29%5C%2Fstatus%5C%2F%28%3F%3Csnowflake%3E%5Cd%2B%29%2F%29%3Bif%28%21match%29%7Balert%28%27%E3%81%93%E3%81%AE%E3%83%9A%E3%83%BC%E3%82%B8%E3%81%AF%20Twitter%20%E3%81%AE%E5%80%8B%E5%88%A5%20Tweet%20%E3%81%A7%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93%27%29%3Breturn%7Dconst%20range%3DgetSearchRange%28match.groups.snowflake%29%3Bconst%20query%3D%60from%3A%24%7Bmatch.groups.name%7D%20since%3A%24%7Brange.since%7D%20until%3A%24%7Brange.until%7D%20include%3Anativeretweets%60%3Blocation.href%3D%60https%3A%2F%2Ftwitter.com%2Fsearch%3Ff%3Dlive%26q%3D%24%7BencodeURIComponent%28query%29%7D%60%7D%3Bmain%28%29%7D%29%28%29%3B">Twitter(現X)の前後ポストを表示する</a><pre>/*
* @title Twitter(現X)の前後ポストを表示する
* @description 個別 Tweet の URL から前後 1 時間の投稿を RT を含めて表示する
* @include https://twitter.com/*
* @license MIT License
* @javascript_url
*/
(() => {
const TWITTER_EPOCH = 1288834974657;
// 'YYYY-MM-DD_HH:mm:ss_JST'形式で日付をフォーマット
const formatTwitterDate = (date) => {
const pad2 = (n) => n.toString().padStart(2, '0');
const year = date.getFullYear();
const month = pad2(date.getMonth() + 1);
const day = pad2(date.getDate());
const hour = pad2(date.getHours());
const minute = pad2(date.getMinutes());
const second = pad2(date.getSeconds());
return `${year}-${month}-${day}_${hour}:${minute}:${second}_JST`;
}
// Snowflake ID 文字列を受け取り、前後 60 分の since, until を返す
const getSearchRange = (snowflakeStr) => {
const snowflake = BigInt(snowflakeStr);
// 41 bits: millisec from TWITTER_EPOCH
// 10 bits: worker_id
// 12 bits: sequence number
// なので 22 bit 右シフトする
const timestamp = Number(snowflake >> 22n) + TWITTER_EPOCH;
return {
since: formatTwitterDate(new Date(timestamp - 60 * 60 * 1000)), // 60 分前
until: formatTwitterDate(new Date(timestamp + 60 * 60 * 1000)), // 60 分後
};
}
const main = () => {
const match = location.href.match(/https:\/\/twitter\.com\/(?<name>[^/]+)\/status\/(?<snowflake>\d+)/);
if (!match) {
alert('このページは Twitter の個別 Tweet ではありません');
return;
}
const range = getSearchRange(match.groups.snowflake);
const query = `from:${match.groups.name} since:${range.since} until:${range.until} include:nativeretweets`;
location.href = `https://twitter.com/search?f=live&q=${encodeURIComponent(query)}`;
}
main();
})();</pre>
-
https://let.hatelabo.jp/Nikola/let/kO26wurQgcAA
2024-03-06T00:57:11Z
リレーからkind10030,kind30030イベントを取得してRabbitにカスタム絵文字リストをインポートする
Nikola
[Let] import emoji list to Rabbit
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkO26wurQgcAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">import emoji list to Rabbit</a><pre>/*
* @title import emoji list to Rabbit
* @description リレーからkind10030,kind30030イベントを取得してRabbitにカスタム絵文字リストをインポートする
* @include https://rabbit.syusui.net/*
* @license CC0 1.0
* @require
*/
(async () => {
if (typeof window === 'undefined') {
await import('websocket-polyfill');
}
const relayUrl = 'wss://relay-jp.nostr.wirednet.jp';//リレーは決め打ちです。すみません。
const getPubkey = async () => {
let pubkey;//Node.jsでデバッグする際はここに初期値を入れてください。無ければ最新の誰かのイベントで代用します。
const nostr = globalThis.window?.nostr;
if (nostr?.getPublicKey) {
try {
pubkey = await nostr.getPublicKey();
} catch (error) {
console.warn(error);
}
}
return pubkey;
};
const getEvent10030 = async (pubkey) => {
return new Promise((resolve) => {
const ws = new WebSocket(relayUrl);
const subscription_id = 'getemojilistinrabbit';
let res;
ws.onopen = () => {
const req = [
'REQ',
subscription_id,
pubkey ? { 'kinds': [10030], 'authors': [pubkey] } : { 'kinds': [10030], 'limit': 1 }
];
ws.send(JSON.stringify(req));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch ( msg[0] ) {
case 'EVENT':
res = msg[2];
break;
case 'EOSE':
ws.send(JSON.stringify(['CLOSE', subscription_id]));
ws.close();
resolve(res);
break;
default:
console.log(msg);
break;
}
};
ws.onerror = () => {
console.error('failed to connect');
}
});
};
const getEmojiList = async (ev10030) => {
const atags = ev10030.tags.filter(tag => tag.length >= 2 && tag[0] === 'a').map(tag => tag[1]);
if (atags.length === 0) {
return [];
}
const filters = [];
for (const atag of atags) {
const ary = atag.split(':');
filters.push({ kinds: [parseInt(ary[0])], authors: [ary[1]], '#d': [ary[2]] });
}
let emojiMap = new Map();
for (const filter of filters) {
await getEvent30030(filter);
}
return emojiMap;
async function getEvent30030(filter) {
return new Promise((resolve) => {
const ws = new WebSocket(relayUrl);
const subscription_id = 'getemojiinrabbit';
ws.onopen = () => {
const req = [
'REQ',
subscription_id,
filter
];
ws.send(JSON.stringify(req));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch ( msg[0] ) {
case 'EVENT':
for (const tag of msg[2].tags.filter(tag => tag.length >= 2 && tag[0] === 'emoji')) {
emojiMap.set(tag[1], tag[2]);
}
break;
case 'EOSE':
ws.send(JSON.stringify(['CLOSE', subscription_id]));
ws.close();
resolve();
break;
default:
console.log(msg);
break;
}
};
ws.onerror = () => {
console.error('failed to connect');
}
});
};
};
const main = async () => {
let pubkey;
try {
pubkey = await getPubkey();
} catch (error) {
console.warn(error);
return;
}
if (globalThis.window && !pubkey) {
console.warn('pubkey is empty.');
return;
}
let ev10030;
try {
ev10030 = await getEvent10030(pubkey);
} catch (error) {
console.warn(error);
return;
}
if (!ev10030) {
return;
}
const emojiMap = await getEmojiList(ev10030);
if (globalThis.window) {
const config = JSON.parse(localStorage['RabbitConfig']);
for (const [key, value] of emojiMap) {
config.customEmojis[key] = { shortcode: key, url: value };
}
localStorage['RabbitConfig'] = JSON.stringify(config);
alert('Complete. Please reload by yourself.');
}
else {
console.log(emojiMap);
console.log('Complete.');
}
};
main();
})();
</pre>
-
https://let.hatelabo.jp/Nikola/let/kO23t_SCgcAA
2024-03-06T00:56:51Z
リレーからkind10000イベントを取得してRabbitにミュートリストをインポートする
Nikola
[Let] import mute list to Rabbit
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2FNikola%2Flet%2FkO23t_SCgcAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">import mute list to Rabbit</a><pre>/*
* @title import mute list to Rabbit
* @description リレーからkind10000イベントを取得してRabbitにミュートリストをインポートする
* @include https://rabbit.syusui.net/*
* @license CC0 1.0
* @require
*/
(async () => {
if (typeof window === 'undefined') {
await import('websocket-polyfill');
}
const relayUrl = 'wss://relay-jp.nostr.wirednet.jp';//リレーは決め打ちです。すみません。
const getPubkey = async () => {
let pubkey;//Node.jsでデバッグする際はここに初期値を入れてください。無ければ最新の誰かのイベントで代用します。
const nostr = globalThis.window?.nostr;
if (nostr?.getPublicKey) {
try {
pubkey = await nostr.getPublicKey();
} catch (error) {
console.warn(error);
}
}
return pubkey;
};
const getEvent10000 = async (pubkey) => {
return new Promise((resolve) => {
const ws = new WebSocket(relayUrl);
const subscription_id = 'getmutelistinrabbit';
let res;
ws.onopen = () => {
const req = [
'REQ',
subscription_id,
pubkey ? { 'kinds': [10000], 'authors': [pubkey] } : { 'kinds': [10000], 'limit': 1 }
];
ws.send(JSON.stringify(req));
};
ws.onmessage = (e) => {
const msg = JSON.parse(e.data);
switch ( msg[0] ) {
case 'EVENT':
res = msg[2];
break;
case 'EOSE':
ws.send(JSON.stringify(['CLOSE', subscription_id]));
ws.close();
resolve(res);
break;
default:
console.log(msg);
break;
}
};
ws.onerror = () => {
console.error('failed to connect');
}
});
};
const getMuteList = async (event, pubkey) => {
let mutedPubkeys = event.tags.filter(tag => tag.length >= 2 && tag[0] === 'p').map(tag => tag[1]) ?? [];
let mutedChannels = event.tags.filter(tag => tag.length >= 2 && tag[0] === 'e').map(tag => tag[1]) ?? [];
let mutedWords = event.tags.filter(tag => tag.length >= 2 && tag[0] === 'word').map(tag => tag[1]) ?? [];
const nostr = globalThis.window?.nostr;
if (event.content && nostr?.nip04?.decrypt) {
try {
const content = await nostr.nip04.decrypt(pubkey, event.content);
const list = JSON.parse(content);
mutedPubkeys = mutedPubkeys.concat(list.filter(tag => tag.length >= 2 && tag[0] === 'p').map(tag => tag[1]));
mutedChannels = mutedChannels.concat(list.filter(tag => tag.length >= 2 && tag[0] === 'e').map(tag => tag[1]));
mutedWords = mutedWords.concat(list.filter(tag => tag.length >= 2 && tag[0] === 'word').map(tag => tag[1]));
} catch (error) {
console.warn(error);
}
}
return [mutedPubkeys, mutedWords];
};
const main = async () => {
let pubkey;
try {
pubkey = await getPubkey();
} catch (error) {
console.warn(error);
return;
}
if (globalThis.window && !pubkey) {
console.warn('pubkey is empty.');
return;
}
let ev10000;
try {
ev10000 = await getEvent10000(pubkey);
} catch (error) {
console.warn(error);
return;
}
const [mutedPubkeysList, mutedKeywordsList] = await getMuteList(ev10000, pubkey);
if (globalThis.window) {
const config = JSON.parse(localStorage['RabbitConfig']);
config.mutedPubkeys = Array.from(new Set([...config.mutedPubkeys, ...mutedPubkeysList]));
config.mutedKeywords = Array.from(new Set([...config.mutedKeywords, ...mutedKeywordsList]));
localStorage['RabbitConfig'] = JSON.stringify(config);
alert('Complete. Please reload by yourself.');
}
else {
console.log({ mutedPubkeysList, mutedKeywordsList});
console.log('Complete.');
}
};
main();
})();
</pre>
-
https://let.hatelabo.jp/onk/let/g5G0uOeEqfcA
2024-03-05T19:56:46Z
[タイトル URL]という形式でページのタイトルとURLをクリップボードにコピーします
onk
[Let] Scrapboxのリンク形式でタイトルとURLをコピー
<a href="javascript:%28%28%29%3D%3E%7B%27use%20strict%27%3Blet%20canonical%3D%28document.querySelector%28%27head%20link%5Brel%3D%22canonical%22%5D%5Bhref%5D%27%29%7C%7C%7B%7D%29.href%3Bif%28canonical%26%26location.hash%29%7Bcanonical%3Dcanonical%2Blocation.hash%7Dconst%20replacedTitle%3Ddocument.title.replaceAll%28%27%5B%27%2C%27%EF%BC%BB%27%29.replaceAll%28%27%5D%27%2C%27%EF%BC%BD%27%29%3Bconst%20content%3D%22%5B%22%2BreplacedTitle%2B%22%20%22%2B%28canonical%7C%7Clocation.href%29%2B%22%5D%22%3Bnavigator.clipboard.writeText%28content%29%7D%29%28%29%3B">Scrapboxのリンク形式でタイトルとURLをコピー</a><pre>/*
* @title Scrapboxのリンク形式でタイトルとURLをコピー
* @description [タイトル URL]という形式でページのタイトルとURLをクリップボードにコピーします
* @include *
* @license MIT License
* @javascript_url
*/
(() => {
'use strict';
let canonical = (document.querySelector('head link[rel="canonical"][href]') || {}).href
if (canonical && location.hash) {
canonical = canonical + location.hash;
}
const replacedTitle = document.title.replaceAll('[', '[').replaceAll(']', ']');
const content = "[" + replacedTitle + " " + (canonical || location.href) + "]";
navigator.clipboard.writeText(content);
})();
</pre>
-
https://let.hatelabo.jp/furyu-tei/let/kOPl-_-QgsAA
2024-02-19T11:39:43Z
Amazon MusicのプレイリストをCSV形式でダウンロード
furyu-tei
[Let] プレイリスト取得
<a href="javascript:%28%28%29%3D%3E%7Bconst%20headers%3D%5B%7Bkey%3A%27no%27%2Cname%3A%27No.%27%7D%2C%7Bkey%3A%27music_title%27%2Cname%3A%27%E6%9B%B2%27%7D%2C%7Bkey%3A%27music_url%27%2Cname%3A%27%E6%9B%B2URL%27%7D%2C%7Bkey%3A%27artists%27%2Cname%3A%27%E3%82%A2%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B9%E3%83%88%27%7D%2C%7Bkey%3A%27artists_url%27%2Cname%3A%27%E3%82%A2%E3%83%BC%E3%83%86%E3%82%A3%E3%82%B9%E3%83%88URL%27%7D%2C%7Bkey%3A%27album_title%27%2Cname%3A%27%E3%82%A2%E3%83%AB%E3%83%90%E3%83%A0%27%7D%2C%7Bkey%3A%27album_url%27%2Cname%3A%27%E3%82%A2%E3%83%AB%E3%83%90%E3%83%A0URL%27%7D%5D%2Ccreate_csv_line%3Dvalues%3D%3Evalues.map%28%28value%3D%3E%60%22%24%7B%28%27%27%2Bvalue%29.replace%28%2F%22%2Fg%2C%27%22%22%27%29%7D%22%60%29%29.join%28%27%2C%27%29%2Cdownload_csv%3D%28csv_filename%2Ccsv_lines%29%3D%3E%7Bconst%20csv_text%3Dcsv_lines.join%28%27%5Cr%5Cn%27%29%2Cbom%3Dnew%20Uint8Array%28%5B239%2C187%2C191%5D%29%2Cblob%3Dnew%20Blob%28%5Bbom%2Ccsv_text%5D%2C%7Btype%3A%27text%2Fcsv%27%7D%29%2Cblob_url%3DURL.createObjectURL%28blob%29%2Cdownload_link%3Ddocument.createElement%28%27a%27%29%3Bdownload_link.href%3Dblob_url%3Bdownload_link.download%3Dcsv_filename%3Bdocument.documentElement.appendChild%28download_link%29%3Bdownload_link.click%28%29%3Bdownload_link.remove%28%29%7D%3Bconst%20header_csv_line%3Dcreate_csv_line%28headers.map%28%28head%3D%3Ehead.name%29%29%29%2Cplaylist_title%3Ddocument.querySelector%28%27music-detail-header.hydrated%5Blabel%5D%27%29.getAttribute%28%27headline%27%29%2Cmusic_info_list%3D%5B...document.querySelectorAll%28%27music-image-row.hydrated%5Bdata-key%5D%27%29%5D.map%28%28%28row%2Cindex%29%3D%3E%7Bconst%20music_title%3Drow.getAttribute%28%27primary-text%27%29%3F%3F%27%27%2Cmusic_url%3Drow.getAttribute%28%27primary-href%27%29%2Cartists%3Drow.getAttribute%28%27secondary-text-1%27%29%3F%3F%27%27%2Cartists_url%3Drow.getAttribute%28%27secondary-href-1%27%29%2Calbum_title%3Drow.getAttribute%28%27secondary-text-2%27%29%3F%3F%27%27%2Calbum_url%3Drow.getAttribute%28%27secondary-href-2%27%29%3Breturn%7Bno%3Aindex%2B1%2Cmusic_title%3Amusic_title%2Cmusic_url%3Amusic_url%3Fnew%20URL%28music_url%2Clocation.href%29.href%3A%27%27%2Cartists%3Aartists%2Cartists_url%3Aartists_url%3Fnew%20URL%28artists_url%2Clocation.href%29.href%3A%27%27%2Calbum_title%3Aalbum_title%2Calbum_url%3Aalbum_url%3Fnew%20URL%28album_url%2Clocation.href%29.href%3A%27%27%7D%7D%29%29%2Ccsv_lines%3D%5Bheader_csv_line%5D.concat%28music_info_list.map%28%28music_info%3D%3Ecreate_csv_line%28headers.map%28%28header%3D%3Emusic_info%5Bheader.key%5D%29%29%29%29%29%29%3Bdownload_csv%28%60%24%7Bplaylist_title%7D.csv%60%2Ccsv_lines%29%7D%29%28%29%3B">プレイリスト取得</a><pre>/*
* @title プレイリスト取得
* @description Amazon MusicのプレイリストをCSV形式でダウンロード
* @include https://music.amazon.co.jp/playlists/*
* @include https://music.amazon.co.jp/my/playlists/*
* @license MIT License
* @javascript_url
*/
(() => {
const
headers = [
{key: 'no', name: 'No.',},
{key: 'music_title', name: '曲',},
{key: 'music_url', name: '曲URL',},
{key: 'artists', name: 'アーティスト',},
{key: 'artists_url', name: 'アーティストURL',},
{key: 'album_title', name: 'アルバム',},
{key: 'album_url', name: 'アルバムURL',},
],
create_csv_line = values => values.map(value => `"${('' + value).replace(/"/g, '""')}"` ).join(','),
download_csv = (csv_filename, csv_lines) => {
const
csv_text = csv_lines.join('\r\n'),
bom = new Uint8Array([0xEF, 0xBB, 0xBF]),
blob = new Blob([bom, csv_text], {'type': 'text/csv'}),
blob_url = URL.createObjectURL(blob),
download_link = document.createElement('a');
download_link.href = blob_url;
download_link.download = csv_filename;
document.documentElement.appendChild(download_link);
download_link.click();
download_link.remove();
};
const
header_csv_line = create_csv_line(headers.map(head => head.name)),
playlist_title = document.querySelector('music-detail-header.hydrated[label]').getAttribute('headline'),
music_info_list = [...document.querySelectorAll('music-image-row.hydrated[data-key]')].map((row, index) => {
const
music_title = row.getAttribute('primary-text') ?? '',
music_url = row.getAttribute('primary-href'),
artists = row.getAttribute('secondary-text-1') ?? '',
artists_url = row.getAttribute('secondary-href-1'),
album_title = row.getAttribute('secondary-text-2') ?? '',
album_url = row.getAttribute('secondary-href-2');
return {
no: index+1,
music_title,
music_url: music_url ? new URL(music_url,location.href).href : '',
artists,
artists_url: artists_url ? new URL(artists_url,location.href).href : '',
album_title,
album_url: album_url ? new URL(album_url,location.href).href : '',
};
}),
csv_lines = [header_csv_line].concat(music_info_list.map(music_info => create_csv_line(headers.map(header => music_info[header.key]))));
download_csv(`${playlist_title}.csv`, csv_lines);
})();
</pre>
-
https://let.hatelabo.jp/mino90/let/kOKK0bOYgsAA
2024-02-18T14:53:16Z
アカウント名を変更しても変わらないDIDを使用したプロフィールページに移動する。プロフィールページで実行
mino90
[Let] Bluesky DIDを使ったプロフィールURLで開き直す
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fmino90%2Flet%2FkOKK0bOYgsAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">Bluesky DIDを使ったプロフィールURLで開き直す</a><pre>/*
* @title Bluesky DIDを使ったプロフィールURLで開き直す
* @description アカウント名を変更しても変わらないDIDを使用したプロフィールページに移動する。プロフィールページで実行
* @include https://bsky.app/profile/*
* @license MIT License
*/
(function(){
if(/^https:\/\/bsky\.app\/profile\//.test(location.href) && !/\/lists\//.test(location.href) && !/\/feed\//.test(location.href) && !/\/post\//.test(location.href) && !/did:plc:/.test(location.href)){
const handle=location.href.match(/^https:\/\/bsky\.app\/profile\/(.+)$/);
let a=new XMLHttpRequest();
a.onreadystatechange = function(){
if (a.readyState == 4 && a.status == 200){
location.href='https://bsky.app/profile/'+a.response.did;
}}
a.open('GET','https://bsky.social/xrpc/com.atproto.identity.resolveHandle?handle='+handle[1]);
a.responseType='json';
a.send();
};
})();
</pre>
-
https://let.hatelabo.jp/Lhankor_Mhy/let/kOGSsI-SgsAA
2024-02-15T08:44:55Z
canonical URLではない場合にページ移動します。
Lhankor_Mhy
[Let] canonical URL に移動
<a href="javascript:%7Bconst%20canonical%3Ddocument.querySelector%28%27%5Brel%3Dcanonical%5D%27%29.href%3Bif%28location.href%21%3D%3Dcanonical%29location.href%3Dcanonical%3Bvoid%200%7D">canonical URL に移動</a><pre>/*
* @title canonical URL に移動
* @description canonical URLではない場合にページ移動します。
* @include http://*
* @license CC0
* @javascript_url
*/
{
const canonical = document.querySelector('[rel=canonical]').href;
if (location.href !== canonical) location.href = canonical;
void 0;
}
</pre>
-
https://let.hatelabo.jp/Cside/let/kNiR2viWgKAA
2024-02-04T03:45:08Z
正規表現で OR 検索したやつを別々の色でハイライト
Cside
[Let] 正規表現で OR 検索したやつを別々の色でハイライト
<a href="javascript:const%20REGEXP%3Dnew%20RegExp%28prompt%28%27RegExp%27%29%2C%27ig%27%29%3Bconst%20getAllTextNodes%3Dcb%3D%3E%7Bconst%20walker%3Ddocument.createTreeWalker%28document.body%2CNodeFilter.SHOW_TEXT%2Cnull%2Cfalse%29%3Blet%20node%3Bwhile%28node%3Dwalker.nextNode%28%29%29%7Bif%28node.textContent.trim%28%29%21%3D%3D%27%27%29cb%28node%29%7D%7D%3Bconst%20createElementFromString%3DhtmlString%3D%3E%7Bconst%20div%3Ddocument.createElement%28%27div%27%29%3Bdiv.innerHTML%3DhtmlString%3Breturn%20div.firstChild%7D%3Bconst%20colors%3D%5B%27yellow%27%2C%27yellowgreen%27%2C%27pink%27%2C%27orange%27%5D%3Bconst%20matched%3Dnew%20Map%3Blet%20lastIndex%3D-1%3BgetAllTextNodes%28%28node%3D%3E%7Bconst%20innerHTML%3Dnode.textContent.replace%28REGEXP%2C%28str%3D%3E%7Bconst%20key%3Dstr.toLowerCase%28%29%3Bif%28matched.has%28key%29%29%7Bconst%20color%3Dmatched.get%28key%29%3Breturn%60%3Cspan%20style%3D%22padding%3A%201px%3B%20color%3A%20black%3B%20background-color%3A%20%24%7Bcolor%7D%3B%22%3E%24%7Bstr%7D%3C%2Fspan%3E%60%7Dconst%20nextColor%3Dcolors%5B%2B%2BlastIndex%5D%3Bmatched.set%28key%2CnextColor%29%3Breturn%60%3Cspan%20style%3D%22padding%3A%201px%3B%20color%3A%20black%3B%20background-color%3A%20%24%7BnextColor%7D%3B%22%3E%24%7Bstr%7D%3C%2Fspan%3E%60%7D%29%29%3Bconst%20replacementNode%3Ddocument.createElement%28%27span%27%29%3BreplacementNode.innerHTML%3DinnerHTML%3Bnode.parentNode.insertBefore%28replacementNode%2Cnode%29%3BsetTimeout%28%28%28%29%3D%3Enode.parentNode.removeChild%28node%29%29%29%7D%29%29%3B">正規表現で OR 検索したやつを別々の色でハイライト</a><pre>/*
* @title 正規表現で OR 検索したやつを別々の色でハイライト
* @description 正規表現で OR 検索したやつを別々の色でハイライト
* @license MIT License
* @javascript_url
*/
const REGEXP = new RegExp(prompt('RegExp'), 'ig')
const getAllTextNodes = (cb) => {
const walker = document.createTreeWalker(
document.body,
NodeFilter.SHOW_TEXT, // テキストノードだけを考慮
null,
false
);
let node;
while ((node = walker.nextNode())) {
if (node.textContent.trim() !== '')
cb(node)
}
};
const createElementFromString = (htmlString) => {
const div = document.createElement('div');
div.innerHTML = htmlString;
return div.firstChild;
};
const colors = ['yellow', 'yellowgreen', 'pink', 'orange', ];
const matched = new Map();
let lastIndex = -1;
getAllTextNodes((node) => {
const innerHTML = node.textContent.replace(REGEXP, (str) => {
const key = str.toLowerCase()
if (matched.has(key)) {
const color = matched.get(key);
return `<span style="padding: 1px; color: black; background-color: ${color};">${str}</span>`
}
const nextColor = colors[++lastIndex];
matched.set(key, nextColor);
return `<span style="padding: 1px; color: black; background-color: ${nextColor};">${str}</span>`;
});
const replacementNode = document.createElement('span');
replacementNode.innerHTML = innerHTML;
node.parentNode.insertBefore(replacementNode, node);
setTimeout(() => node.parentNode.removeChild(node)); // 非同期じゃないと何故かエラーに...
});</pre>
-
https://let.hatelabo.jp/Lhankor_Mhy/let/kNe6m5vsgKAA
2024-01-31T08:34:19Z
はてなグリーンスターに縁をつけます。via: https://b.hatena.ne.jp/entry/4748592896526814671/comment/murlock
Lhankor_Mhy
[Let] はてなグリーンスターに縁をつける
<a href="javascript:%7Blet%20timerId%3Bnew%20MutationObserver%28%28%28mutationList%2Cobserver%29%3D%3E%7BmutationList.forEach%28%28mutation%3D%3E%7Bswitch%28mutation.type%29%7Bcase%22childList%22%3AclearTimeout%28timerId%29%3BtimerId%3DsetTimeout%28replaceGreenStar%2C500%29%3Bbreak%7D%7D%29%29%7D%29%29.observe%28document.body%2C%7BchildList%3Atrue%2Csubtree%3Atrue%7D%29%3Bconst%20replaceGreenStar%3D%28%29%3D%3E%7Bdocument.querySelectorAll%28%27.hatena-star-star.green%20span%27%29%3F.forEach%28%28e%3D%3Ee.style.backgroundImage%3D%60url%28%22data%3Aimage%2Fsvg%2Bxml%2C%253Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20width%3D%2711%27%20height%3D%2710.299%27%20viewBox%3D%270%200%2011%2010.299%27%253E%253Cpath%20d%3D%27M-4785.954%2C1552.838l2.935%2C2.125-1.127%2C3.381a.352.352%2C0%2C0%2C0%2C.538.393l2.815-1.961h0l.2-.14%2C3.009%2C2.1a.353.353%2C0%2C0%2C0%2C.533-.4l-1.036-3.128v0l-.079-.24.2-.15%2C0%2C0%2C2.712-1.971a.352.352%2C0%2C0%2C0-.207-.638h-3.651l-.079-.238%2C0%2C0-1.075-3.218a.352.352%2C0%2C0%2C0-.667-.005l-1.167%2C3.464h-3.653A.353.353%2C0%2C0%2C0-4785.954%2C1552.838Z%27%20transform%3D%27translate%284786.096%20-1548.503%29%27%20fill%3D%27%252300d200%27%20stroke%3D%27%2523000%27%2F%253E%253C%2Fsvg%253E%22%29%60%29%29%7D%3BreplaceGreenStar%28%29%7D">はてなグリーンスターに縁をつける</a><pre>// ==UserScript==
// @name はてなグリーンスターに縁をつける
// @title はてなグリーンスターに縁をつける
// @namespace https://let.hatelabo.jp/Lhankor_Mhy/let/kNe6m5vsgKAA
// @version 2024-01-31
// @description はてなグリーンスターに縁をつけます。via: https://b.hatena.ne.jp/entry/4748592896526814671/comment/murlock
// @author Lhankor_Mhy
// @match https://b.hatena.ne.jp/entry/*
// @match https://b.hatena.ne.jp/*/bookmark
// @icon https://b.hatena.ne.jp/favicon.ico
// @license CC0
// @noframes
// @grant none
// @javascript_url
// ==/UserScript==
{
let timerId;
new MutationObserver((mutationList, observer) => {
mutationList.forEach((mutation) => {
switch (mutation.type) {
case "childList":
clearTimeout(timerId);
timerId = setTimeout(replaceGreenStar, 500)
break;
}
});
}).observe(document.body, {
childList: true,
subtree: true,
});
const replaceGreenStar = () => {
document.querySelectorAll('.hatena-star-star.green span')?.forEach(e =>e.style.backgroundImage = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='11' height='10.299' viewBox='0 0 11 10.299'%3E%3Cpath d='M-4785.954,1552.838l2.935,2.125-1.127,3.381a.352.352,0,0,0,.538.393l2.815-1.961h0l.2-.14,3.009,2.1a.353.353,0,0,0,.533-.4l-1.036-3.128v0l-.079-.24.2-.15,0,0,2.712-1.971a.352.352,0,0,0-.207-.638h-3.651l-.079-.238,0,0-1.075-3.218a.352.352,0,0,0-.667-.005l-1.167,3.464h-3.653A.353.353,0,0,0-4785.954,1552.838Z' transform='translate(4786.096 -1548.503)' fill='%2300d200' stroke='%23000'/%3E%3C/svg%3E")`)
}
replaceGreenStar();
}
</pre>
-
https://let.hatelabo.jp/yuta25/let/jZ-L6ou2goAA
2024-01-27T01:25:18Z
便利
yuta25
[Let] Amazonの本を国会図書館で検索する
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fyuta25%2Flet%2FjZ-L6ou2goAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">Amazonの本を国会図書館で検索する</a><pre>/*
* @title Amazonの本を国会図書館で検索する
* @description 便利
* @include https://www.amazon.co.jp/*
* @license MIT License
*/
(() => {
const title = document
.querySelector("#productTitle")
.textContent.trim()
.replaceAll(/[~‐‑‒–—―⁃−⎯⏤─━﹘﹣-ー-]/g, " ")
.replaceAll(/[()(){}{}[]\[\]【】〈〉〔〕]/g, " ");
window.open(
`https://ndlsearch.ndl.go.jp/search?cs=bib&display=panel&from=0&size=20&keyword=${encodeURIComponent(
title
)}&f-ht=ndl&f-ht=library&f-mt=dtbook`
);
})();
</pre>
-
https://let.hatelabo.jp/hadsn/let/kMjx1IOogcAA
2024-01-08T16:27:23Z
NAVITIMEの任意の時刻表を、東急向けのNAVITIE電鉄・バス事業者向けソリューションで開くやつ
hadsn
[Let] NAVITIME時刻表大東急lize
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fhadsn%2Flet%2FkMjx1IOogcAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">NAVITIME時刻表大東急lize</a><pre>/*
* @title NAVITIME時刻表大東急lize
* @description NAVITIMEの任意の時刻表を、東急向けのNAVITIE電鉄・バス事業者向けソリューションで開くやつ
* @include http://www.navitime.co.jp
* @license MIT License
* @require
*/
javascript:(() =>
{
var indexOfQuestion = document.URL.lastIndexOf('?') + 1;
var params;
if(indexOfQuestion == 0){return -1;}
else
{
console.log(params = document.URL.slice(indexOfQuestion).split('&'));
for(var i = 0; i < params.length; i++)
{
if(params[i].indexOf('node=') != -1){stCd = params[i].slice(params[i].indexOf('node=') + 5)}
if(params[i].indexOf('lineId=') != -1){rrCd = params[i].slice(params[i].indexOf('lineId=') + 7)}
if(params[i].indexOf('updown=') != -1){updown = params[i].slice(params[i].indexOf('updown=') + 7)}
}
location.href = 'https://transfer.navitime.biz/tokyu/pc/diagram/TrainDiagram?' + 'stCd=' + stCd + '&rrCd=' + rrCd + '&updown=' + updown;
}
}
)()</pre>
-
https://let.hatelabo.jp/mino90/let/kL7dzrjmgoAA
2023-12-23T21:46:46Z
ついているはてなスターを見たり、はてなスターをつけられるあのパネルを開く
mino90
[Let] はてなスターパネルを開く
<a href="javascript:%28function%28%29%7Bwindow.open%28%27https%3A%2F%2Fs.hatena.ne.jp%2Fjs%2Fwidget%2Fentry.html%3Furl%3D%27%2BencodeURIComponent%28location.href%29%2B%27%26title%3D%27%2BencodeURIComponent%28document.title%29%2C%27_blank%27%2C%27width%3D380%2Cheight%3D480%2Cnoopener%3Dyes%2Cnoreferrer%3Dyes%27%29%3Bvoid%200%7D%29%28%29%3B">はてなスターパネルを開く</a><pre>/*
* @title はてなスターパネルを開く
* @description ついているはてなスターを見たり、はてなスターをつけられるあのパネルを開く
* @include http://*
* @license MIT License
* @javascript_url
*/
(function(){
window.open('https://s.hatena.ne.jp/js/widget/entry.html?url='+encodeURIComponent(location.href)+'&title='+encodeURIComponent(document.title),'_blank','width=380,height=480,noopener=yes,noreferrer=yes');void(0);
})();</pre>
-
https://let.hatelabo.jp/Lhankor_Mhy/let/jun3wcKygeAA
2023-12-19T09:52:57Z
ブックマークレットを使ってからメールスレッドやタイトルなどをクリックしてください
Lhankor_Mhy
[Let] Gmailのパーマリンク取得
<a href="javascript:%7Bconst%20targetDataAttributeNames%3D%5B%22data-legacy-last-message-id%22%2C%22data-legacy-message-id%22%2C%22data-legacy-thread-id%22%5D%3Balert%28%27%E3%83%91%E3%83%BC%E3%83%9E%E3%83%AA%E3%83%B3%E3%82%AF%E3%82%92%E5%8F%96%E5%BE%97%E3%81%99%E3%82%8B%E3%83%A1%E3%83%BC%E3%83%AB%E3%82%92%E3%82%AF%E3%83%AA%E3%83%83%E3%82%AF%27%29%3Bconsole.log%28%27test%27%29%3Bdocument.documentElement.addEventListener%28%27click%27%2C%28e%3D%3E%7Bconst%20target%3De.target%3Blet%20id%3Bfor%28const%20targetDataAttribute%20of%20targetDataAttributeNames%29%7Bif%28target.getAttributeNames%28%29.includes%28targetDataAttribute%29%29%7Bid%3Dtarget.getAttribute%28targetDataAttribute%29%3Bbreak%7Dif%28target.closest%28%60%5B%24%7BtargetDataAttribute%7D%5D%60%29%29%7Bid%3Dtarget.closest%28%60%5B%24%7BtargetDataAttribute%7D%5D%60%29.getAttribute%28targetDataAttribute%29%3Bbreak%7D%7Dprompt%28%27%E3%83%91%E3%83%BC%E3%83%9E%E3%83%AA%E3%83%B3%E3%82%AF%27%2C%60https%3A%2F%2Fmail.google.com%2Fmail%2Fu%2F0%2F%23all%2F%24%7Bid%7D%60%29%7D%29%2C%7Bonce%3Atrue%7D%29%7D">Gmailのパーマリンク取得</a><pre>/*
* @title Gmailのパーマリンク取得
* @description ブックマークレットを使ってからメールスレッドやタイトルなどをクリックしてください
* @include https://mail.google.com/
* @license CC0
* @javascript_url
*/
{
const targetDataAttributeNames = [
"data-legacy-last-message-id",
"data-legacy-message-id",
"data-legacy-thread-id",
]
alert('パーマリンクを取得するメールをクリック')
console.log('test')
document.documentElement.addEventListener('click', e => {
const target = e.target;
let id;
for (const targetDataAttribute of targetDataAttributeNames) {
if (target.getAttributeNames().includes(targetDataAttribute)) {
id = target.getAttribute(targetDataAttribute);
break;
}
if (target.closest(`[${targetDataAttribute}]`)) {
id = target.closest(`[${targetDataAttribute}]`).getAttribute(targetDataAttribute);
break;
}
}
prompt('パーマリンク', `https://mail.google.com/mail/u/0/#all/${id}`)
}, { once: true })
}
</pre>
-
https://let.hatelabo.jp/furyu-tei/let/kLvm7Oz0gOAA
2023-12-19T08:36:39Z
Twitter(X)のユーザープロフィールからメディア検索画面に遷移する
furyu-tei
[Let] メディア検索画面に遷移
<a href="javascript:%28%28%29%3D%3E%7B%27use%20strict%27%3Bconst%20media_tab%3Ddocument.querySelector%28%27%5Brole%3D%22presentation%22%5D%20a%5Bhref%24%3D%22%2Fmedia%22%5D%5Brole%3D%22tab%22%5D%27%29%3Bif%28%21media_tab%29%7Breturn%7Dconst%20username%3Dmedia_tab.href.match%28%2F%28%5B%5E%2F%5D%2A%29%5C%2Fmedia%24%2F%29%5B1%5D%2Cquery%3D%60from%3A%24%7Busername%7D%20%28filter%3Amedia%20OR%20card_name%3Aanimated_gif%29%60%2Csearch_url%3D%60%2Fsearch%3Fq%3D%24%7BencodeURIComponent%28query%29%7D%26src%3Dtyped_query%26f%3Dlive%60%2Curl_object%3Dnew%20URL%28search_url%2Clocation.href%29%3Btry%7Bconst%20previous_url_object%3Dnew%20URL%28location.href%29%2Cstate%3D%7Bkey%3A%27r80bpk%27%2Cstate%3A%7BfromApp%3Atrue%2CpreviousPath%3Aprevious_url_object.pathname%2Bprevious_url_object.search%7D%7D%2Cpop_state_event%3Dnew%20PopStateEvent%28%27popstate%27%2C%7Bstate%3Astate%7D%29%3Bhistory.pushState%28state%2C%27%27%2Curl_object.pathname%2Burl_object.search%29%3BdispatchEvent%28pop_state_event%29%7Dcatch%28error%29%7Blocation.href%3Dsearch_url%7D%7D%29%28%29%3B">メディア検索画面に遷移</a><pre>/*
* @title メディア検索画面に遷移
* @description Twitter(X)のユーザープロフィールからメディア検索画面に遷移する
* @include https://twitter.com/*
* @include https://x.com/*
* @license MIT License
* @javascript_url
*/
(() => {
'use strict';
const
media_tab = document.querySelector('[role="presentation"] a[href$="/media"][role="tab"]');
if (! media_tab) {
return;
}
const
username = media_tab.href.match(/([^/]*)\/media$/)[1],
query = `from:${username} (filter:media OR card_name:animated_gif)`,
search_url = `/search?q=${encodeURIComponent(query)}&src=typed_query&f=live`,
url_object = new URL(search_url, location.href);
try {
const
previous_url_object = new URL(location.href),
state = {
key: 'r80bpk',
state: {
fromApp: true,
previousPath: previous_url_object.pathname + previous_url_object.search,
},
},
pop_state_event = new PopStateEvent('popstate', {state: state});
history.pushState(state, '', url_object.pathname + url_object.search);
dispatchEvent(pop_state_event);
}
catch (error) {
location.href = search_url;
}
})();
</pre>
-
https://let.hatelabo.jp/mino90/let/kLrt1avEgMAA
2023-12-17T21:18:49Z
60秒間再生するとログイン画面をポップアップして一時停止させられるのでその前に変数を上書きする
mino90
[Let] bilibili ログアウト状態で停止させないUserscript
<a href="javascript:%22https%3A%2F%2Flet.st-hatelabo.com%2Fmino90%2Flet%2FkLrt1avEgMAA.bookmarklet.js%20%28arg%29%22.replace%28%2F%28%5CS%2B%29%5Cs%2B%28%5CS%2A%29%2F%2Cfunction%28s%2Curl%2Carg%29%7Bs%3Ddocument.createElement%28%22script%22%29%3Bs.charset%3D%22utf-8%22%3Bs.src%3Durl%2B%22%3Fs%3D%22%2BencodeURIComponent%28arg%29%3Bdocument.body.appendChild%28s%29%7D%29%3Bvoid%280%29%3B">bilibili ログアウト状態で停止させないUserscript</a><pre>/*
* @title bilibili ログアウト状態で停止させないUserscript
* @description 60秒間再生するとログイン画面をポップアップして一時停止させられるのでその前に変数を上書きする
* @include https://www.bilibili.com/video/*
* @license MIT License
*/
//天安門広場
setTimeout(
function() {
let s = document.createElement('script');
s.innerHTML='__INITIAL_STATE__.user.isLogin=true;';
document.head.appendChild(s);
},50000);
</pre>
-
https://let.hatelabo.jp/furyu-tei/let/hJmdpf3zhZcA
2023-11-11T02:47:49Z
Twitterの個別ツイート(https://twitter.com/<name>/status/<id>)のURL(<id>)からおおよその投稿時刻を取得(2010年11月4日22時(UTC)以降のもの)
furyu-tei
[Let] ツイートID→時刻変換
<a href="javascript:%28%28%29%3D%3E%7B%27use%20strict%27%3Bconst%20twitter_epoch_time%3D1288834974657n%2Cthreshold_tweet_id%3D300000000000000n%3Blet%20tweet_id%3D%28new%20URL%28location.href%29.pathname.match%28%2F%5E%5C%2F%5B%5E%2F%5D%2B%5C%2Fstatus%5C%2F%28%5Cd%2B%29%2F%29%7C%7C%5B%5D%29%5B1%5D%3Bif%28%21tweet_id%29%7Btweet_id%3Dprompt%28%27Tweet%20ID%27%29%3Bif%28%21tweet_id%29%7Breturn%7D%7Dtweet_id%3DBigInt%28tweet_id%29%3Bif%28tweet_id%3Cthreshold_tweet_id%29%7Balert%28%60Unsupported%20Tweet%20ID%3A%20%24%7Btweet_id%7D%20%28%3C%20%24%7Bthreshold_tweet_id%7D%2C%20before%202010%2F11%2F04%29%60%29%3Breturn%7Dlet%20timestamp_ms%3DNumber%28twitter_epoch_time%2B%28tweet_id%3E%3E22n%29%29%2Cdate_obj%3Dnew%20Date%28timestamp_ms%29%2Ciso_timestring%3Ddate_obj.toISOString%28%29.replace%28%2F%5C.%5Cd%2BZ%24%2F%2C%27Z%27%29%3Bdate_obj.setMinutes%28date_obj.getMinutes%28%29-date_obj.getTimezoneOffset%28%29%29%3Blet%20timestring%3Ddate_obj.toISOString%28%29.replace%28%2FT%2F%2C%27%20%27%29.replace%28%2F%5C.%5Cd%2BZ%24%2F%2C%27%27%29%2Cprompt_text%3D%60%5Cn%5BTweet%20ID%5D%20%24%7Btweet_id%7D%5Cn%5BISO%208601%5D%20%24%7Biso_timestring%7D%60%2Cresult%3Dprompt%28prompt_text%2Ctimestring%29%3Bif%28result%21%3D%3Dnull%29%7Bnavigator.clipboard.writeText%28result%29.catch%28%28error%3D%3Econsole.info%28%60Copy%20to%20clipboard%20failed.%20%28%24%7Berror%7D%29%60%29%29%29.then%28%28%28%29%3D%3Econsole.debug%28%60Copy%20to%20clipboard%20succeeded.%20%28%24%7Bresult%7D%29%60%29%29%29%7D%7D%29%28%29%3B">ツイートID→時刻変換</a><pre>/*
* @title ツイートID→時刻変換
* @description Twitterの個別ツイート(https://twitter.com/<name>/status/<id>)のURL(<id>)からおおよその投稿時刻を取得(2010年11月4日22時(UTC)以降のもの)
* @include https://twitter.com/*
* @license MIT License
* @javascript_url
*/
(()=>{
'use strict';
const
twitter_epoch_time = 1288834974657n, // https://www.slideshare.net/pfi/id-15755280
threshold_tweet_id = 300000000000000n;
let tweet_id = (new URL(location.href).pathname.match(/^\/[^/]+\/status\/(\d+)/)||[])[1];
if (! tweet_id) {
tweet_id = prompt('Tweet ID');
if (! tweet_id) {
return;
}
}
tweet_id = BigInt(tweet_id);
if (tweet_id < threshold_tweet_id) {
alert(`Unsupported Tweet ID: ${tweet_id} (< ${threshold_tweet_id}, before 2010/11/04)`);
return;
}
let timestamp_ms = Number(twitter_epoch_time + (tweet_id>>22n)),
date_obj = new Date(timestamp_ms),
iso_timestring = date_obj.toISOString().replace(/\.\d+Z$/, 'Z');
date_obj.setMinutes(date_obj.getMinutes() - date_obj.getTimezoneOffset());
let timestring = date_obj.toISOString().replace(/T/, ' ').replace(/\.\d+Z$/, ''),
prompt_text = `
[Tweet ID] ${tweet_id}
[ISO 8601] ${iso_timestring}`,
result = prompt(prompt_text, timestring);
if (result !== null) {
navigator.clipboard.writeText(result)
.catch(error => console.info(`Copy to clipboard failed. (${error})`))
// TODO: Firefox だとエラーになる (90.0.2)
// Uncaught (in promise) DOMException: Clipboard write was blocked due to lack of user activation.
.then(() => console.debug(`Copy to clipboard succeeded. (${result})`));
}
})();
</pre>
-
https://let.hatelabo.jp/onk/let/kJmSpcv0geAA
2023-10-26T12:22:27Z
はてなブックマークのコメント一覧ページに遷移する
onk
[Let] b
<a href="javascript:%28%28%29%3D%3E%7Blocation.href%3D%60https%3A%2F%2Fb.hatena.ne.jp%2Fentry%2F%24%7Blocation.href.replace%28%27%23%27%2C%27%2523%27%29%7D%60%7D%29%28%29%3B">b</a><pre>/*
* @title b
* @description はてなブックマークのコメント一覧ページに遷移する
* @include http://*
* @license MIT License
* @javascript_url
*/
(() => {
location.href = `https://b.hatena.ne.jp/entry/${location.href.replace('#', '%23')}`;
})();</pre>
-
https://let.hatelabo.jp/labocho/let/kJTI6sDEgMAA
2023-10-19T09:52:25Z
my bookmarklet
labocho
[Let] Remove disableremoteplayback
<a href="javascript:document.querySelectorAll%28%22video%5Bdisableremoteplayback%5D%22%29.forEach%28%28function%28e%29%7Be.removeAttribute%28%22disableremoteplayback%22%29%7D%29%29%3B">Remove disableremoteplayback</a><pre>/*
* @title Remove disableremoteplayback
* @description my bookmarklet
* @include https://*
* @license MIT License
* @javascript_url
*/
document.querySelectorAll("video[disableremoteplayback]").forEach(function(e) { e.removeAttribute("disableremoteplayback") })
</pre>