Skip to content

Instantly share code, notes, and snippets.

@xkikeg
Created July 27, 2026 08:23
Show Gist options
  • Select an option

  • Save xkikeg/9f34153802d5aba989e2261fe7eae341 to your computer and use it in GitHub Desktop.

Select an option

Save xkikeg/9f34153802d5aba989e2261fe7eae341 to your computer and use it in GitHub Desktop.
Swisspass to CSV Tampermonkey csript
// ==UserScript==
// @name Swisspass Halbtax Plus
// @namespace http://tampermonkey.net/
// @version 1.0.0
// @description Get CSV out of Halbtax Plus
// @author kikeg
// @match https://www.swisspass.ch/info/abos/detailRefilledContract/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=swisspass.ch
// @grant GM.setClipboard
// ==/UserScript==
(function () {
'use strict';
const HEADING_TEXT = 'Saldoverlauf in CHF';
const BTN_ID = 'saldoverlauf-csv-btn';
// --- ユーティリティ ---------------------------------------------------
const norm = (s) => (s || '').replace(/\u00a0/g, ' ').replace(/\s+/g, ' ').trim();
/** 対象の h3 を探す */
function findHeading() {
return [...document.querySelectorAll('h3')]
.find((h) => norm(h.textContent) === HEADING_TEXT) || null;
}
/** h3 の後続の兄弟から table を探す */
function findTable(h3) {
let el = h3.nextElementSibling;
while (el) {
if (el.tagName === 'TABLE') return el;
const t = el.querySelector('table');
if (t) return t;
// 次の見出しまで来たら打ち切り
if (/^H[1-6]$/.test(el.tagName)) return null;
el = el.nextElementSibling;
}
return null;
}
/** CSV の1セルをエスケープ */
function esc(v) {
const s = norm(v);
return /[",\n\r;]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
}
/** table -> CSV文字列 */
function tableToCsv(table) {
const rows = [...table.querySelectorAll('tr')];
return rows
.map((tr) =>
[...tr.querySelectorAll('th,td')]
.map((cell) => esc(cell.innerText ?? cell.textContent))
.join(',')
)
.join('\r\n');
}
/** GM.setClipboard / GM_setClipboard / navigator.clipboard の順で試す */
async function copyToClipboard(text) {
if (typeof GM !== 'undefined' && GM && typeof GM.setClipboard === 'function') {
await GM.setClipboard(text, 'text');
return;
}
if (typeof GM_setClipboard === 'function') {
GM_setClipboard(text, 'text');
return;
}
// 最終フォールバック
await navigator.clipboard.writeText(text);
}
// --- ボタン設置 -------------------------------------------------------
function insertButton(h3) {
if (document.getElementById(BTN_ID)) return;
const btn = document.createElement('button');
btn.id = BTN_ID;
btn.type = 'button';
btn.textContent = 'CSVをコピー';
btn.style.cssText =
'margin:8px 0;padding:6px 14px;font-size:13px;cursor:pointer;' +
'border:1px solid #888;border-radius:4px;background:#f5f5f5;';
btn.addEventListener('click', async (e) => {
e.preventDefault();
const table = findTable(h3);
if (!table) {
alert('テーブルが見つかりませんでした。読み込み完了後にもう一度お試しください。');
return;
}
try {
await copyToClipboard(tableToCsv(table));
const orig = btn.textContent;
btn.textContent = 'コピーしました ✓';
btn.disabled = true;
setTimeout(() => {
btn.textContent = orig;
btn.disabled = false;
}, 1500);
} catch (err) {
console.error(err);
alert('クリップボードへのコピーに失敗しました: ' + err);
}
});
h3.insertAdjacentElement('afterend', btn);
}
// --- 出現監視(AJAX対応) --------------------------------------------
function tryInit() {
const h3 = findHeading();
if (!h3) return false;
// テーブルが出来上がってからボタンを出す
if (!findTable(h3)) return false;
insertButton(h3);
return true;
}
// 1) まず即時チェック
if (!tryInit()) {
// 2) DOM変更を監視(AJAX描画・再描画の両方に対応)
const observer = new MutationObserver(() => {
tryInit(); // 見つかれば設置。消えたら再設置される。
});
observer.observe(document.documentElement, { childList: true, subtree: true });
// 3) 保険:一定間隔でもチェック(30秒で停止)
const timer = setInterval(tryInit, 1000);
setTimeout(() => clearInterval(timer), 30000);
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment