// ==UserScript==
// @name アニメイト通販:カートと「あとで買う」をMarkdownでコピー
// @namespace hollen9.com
// @version 1.0.0
// @description アニメイトオンラインショップのカートと「あとで買う」商品のリンクをMarkdown形式でクリップボードへコピーします。
// @match https://www.animate-onlineshop.jp/cart/*
// @match https://www.animate-onlineshop.jp/cart/index.php*
// @grant GM_setClipboard
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Build absolute URL from <a> element
const absUrl = (a) => new URL(a.getAttribute('href'), location.origin).toString();
// Sanitize text: remove zero-width chars, collapse spaces, trim
const clean = (s) =>
(s || '')
.replace(/[\u200B-\u200D\uFEFF]/g, '')
.replace(/\s+/g, ' ')
.trim();
// Get item title from a <tr> with multiple fallbacks
function getTitleFromRow(tr) {
// Primary: <h3><a>
let a = tr.querySelector('.cart_item_info h3 a');
// Fallback: any product link in detail area
if (!a) a = tr.querySelector('.cart_item_detail a[href^="/pd/"]');
if (!a) return '';
// Prefer visible text
let t = clean(a.textContent || a.innerText || '');
// Fallback to title attribute
if (!t) t = clean(a.getAttribute('title'));
// Fallback to image alt in the same row
if (!t) {
const img = tr.querySelector('.cart_item_detail img[alt]');
if (img) t = clean(img.getAttribute('alt'));
}
return t;
}
// Extract items (title, url, price, qty, release) from a cart section
function extractItemsFromSection(sectionEl) {
const rows = sectionEl.querySelectorAll('tbody > tr');
const items = [];
rows.forEach((tr) => {
const link = tr.querySelector('.cart_item_info h3 a, .cart_item_detail a[href^="/pd/"]');
if (!link) return;
const title = getTitleFromRow(tr) || '(no title)';
const url = absUrl(link);
const priceEl = tr.querySelector('.cart_item_price');
const price = clean(priceEl ? priceEl.textContent : '');
// Quantity: cart uses <span class="num">1</span>; buy-later shows "数量:1"
let qty = 1;
const qtyEl = tr.querySelector('.fl_cart_item_num .num');
if (qtyEl) {
const m = clean(qtyEl.textContent).match(/(\d+)/);
if (m) qty = parseInt(m[1], 10);
}
// Release text (optional)
let release = '';
const releases = tr.querySelectorAll('.cart_item_release');
releases.forEach((p) => {
const t = clean(p.textContent);
if (t.includes('発売日')) release = t;
});
items.push({ title, url, price, qty, release });
});
return items;
}
// Build Markdown output
function buildMarkdown(cartItems, laterItems) {
const lines = [];
if (cartItems.length) {
lines.push('## カート');
cartItems.forEach((it) => {
lines.push(`- [${it.title}](${it.url}) ×${it.qty} — ${it.price}${it.release ? ` — ${it.release}` : ''}`);
});
lines.push('');
}
if (laterItems.length) {
lines.push('## あとで買う');
laterItems.forEach((it) => {
lines.push(`- [${it.title}](${it.url}) ×${it.qty} — ${it.price}${it.release ? ` — ${it.release}` : ''}`);
});
lines.push('');
}
lines.push(`_from: ${location.href}_`);
return lines.join('\n');
}
// Copy text to clipboard (GM_setClipboard → navigator.clipboard → fallback)
function copyToClipboard(text) {
try {
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text, { type: 'text', mimetype: 'text/plain' });
return Promise.resolve();
}
} catch {}
if (navigator.clipboard?.writeText) return navigator.clipboard.writeText(text);
const ta = document.createElement('textarea');
ta.value = text;
document.body.appendChild(ta);
ta.select();
document.execCommand('copy');
ta.remove();
return Promise.resolve();
}
// Simple toast UI
function toast(msg) {
const t = document.createElement('div');
t.textContent = msg;
Object.assign(t.style, {
position: 'fixed',
right: '16px',
bottom: '72px',
background: 'rgba(0,0,0,0.88)',
color: '#fff',
padding: '8px 12px',
borderRadius: '10px',
fontSize: '12px',
zIndex: 999999,
maxWidth: '60vw',
lineHeight: 1.5,
});
document.body.appendChild(t);
setTimeout(() => t.remove(), 2600);
}
// Create floating action button (Japanese UI)
function makeButton() {
const btn = document.createElement('button');
btn.textContent = 'カートをMarkdownでコピー';
Object.assign(btn.style, {
position: 'fixed',
right: '16px',
bottom: '16px',
padding: '10px 14px',
background: '#00a0e9',
color: '#fff',
border: 'none',
borderRadius: '12px',
fontWeight: '600',
cursor: 'pointer',
boxShadow: '0 6px 18px rgba(0,0,0,0.2)',
zIndex: 999999,
});
btn.addEventListener('click', async () => {
try {
// Find sections: first cart section = main cart, section with h2 "あとで買う"
const sections = Array.from(document.querySelectorAll('section.cart'));
let cartSection = null;
let laterSection = null;
sections.forEach((sec) => {
const h2 = sec.querySelector('h2');
if (h2 && h2.textContent.includes('あとで買う')) {
laterSection = sec;
} else if (!cartSection) {
cartSection = sec;
}
});
const cartItems = cartSection ? extractItemsFromSection(cartSection) : [];
const laterItems = laterSection ? extractItemsFromSection(laterSection) : [];
if (!cartItems.length && !laterItems.length) {
toast('商品が見つかりません。カートページにいるかご確認ください。');
return;
}
const md = buildMarkdown(cartItems, laterItems);
await copyToClipboard(md);
toast(`Markdownとしてコピーしました(合計 ${cartItems.length + laterItems.length} 件)。\nコンソールにも出力しました。`);
console.log('=== アニメイト カート Markdown ===\n' + md);
} catch (err) {
console.error(err);
toast('コピーに失敗しました。開発者ツール(F12)のコンソールをご確認ください。');
}
});
document.body.appendChild(btn);
}
// Init
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', makeButton);
} else {
makeButton();
}
})();