Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Deploy static site to GitHub Pages

on:
push:
branches:
- main
- master
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure Pages
uses: actions/configure-pages@v5

- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: .

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
# -
# My Spy Code

這個專案是可直接部署到 GitHub Pages 的靜態網站。

## 預設頁面
- `index.html`(由 `客戶.html` 同步而來)

## 啟用 GitHub Pages
1. 將此 repo push 到 GitHub(建議 repo 名稱:`ferzzzzzz-tools`)。
2. 到 **Settings → Pages**,Source 選 **GitHub Actions**。
3. push 到 `main` 或 `master` 後,`.github/workflows/pages.yml` 會自動部署。

## 預期網址
- 若帳號為 `<你的 GitHub 帳號>` 且 repo 名稱為 `ferzzzzzz-tools`,網址會是:
- `https://<你的 GitHub 帳號>.github.io/ferzzzzzz-tools/`
332 changes: 332 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,332 @@
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>My Spy Code|專屬加密摩斯</title>
<style>
:root {
--bg: #0f172a;
--panel: #111827;
--panel-2: #1f2937;
--text: #e5e7eb;
--muted: #9ca3af;
--accent: #22d3ee;
--good: #34d399;
--warn: #fbbf24;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "Noto Sans TC", "Segoe UI Emoji", sans-serif;
background: radial-gradient(circle at top, #1e293b, var(--bg));
color: var(--text);
min-height: 100vh;
padding: 24px;
}
.wrap {
max-width: 960px;
margin: 0 auto;
display: grid;
gap: 16px;
}
.card {
background: linear-gradient(160deg, var(--panel), var(--panel-2));
border: 1px solid #334155;
border-radius: 14px;
padding: 18px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
}
h1 { margin: 0 0 8px; font-size: 1.5rem; }
p { margin: 6px 0; color: var(--muted); }
textarea {
width: 100%;
min-height: 120px;
border-radius: 10px;
border: 1px solid #475569;
background: #0b1220;
color: var(--text);
padding: 12px;
resize: vertical;
font-size: 1rem;
}
.row { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
button {
border: none;
border-radius: 10px;
padding: 10px 14px;
font-size: 0.95rem;
color: #04121a;
background: var(--accent);
font-weight: 700;
cursor: pointer;
}
button.secondary { background: #c4b5fd; }
button.good { background: var(--good); }
button.warn { background: var(--warn); }
.output {
margin-top: 12px;
background: #020617;
border: 1px solid #334155;
border-radius: 10px;
padding: 12px;
word-break: break-word;
line-height: 1.7;
white-space: pre-wrap;
}
.small { font-size: 0.85rem; color: var(--muted); }
.pill {
display: inline-block;
padding: 3px 8px;
border: 1px solid #64748b;
border-radius: 99px;
margin-right: 6px;
margin-top: 6px;
font-size: 0.8rem;
color: #cbd5e1;
}
</style>
</head>
<body>
<main class="wrap">
<section class="card">
<h1>🕵️ My Spy Code(你的專屬加密摩斯)</h1>
<p>不是國際通用摩斯,而是自訂「My Spy Code」。可加密:中文、英文、數字、特殊符號、表情符號。</p>
<p class="small">規則:先把文字轉成 UTF-8(可處理所有字元),每個 16 進位位元再轉成自訂摩斯碼;以 <code>/</code> 分隔每個位元組。</p>
<div>
<span class="pill">範例:# = ....--</span>
<span class="pill">支援 emoji 名稱前處理</span>
</div>
</section>

<section class="card">
<h2>1) 輸入原文</h2>
<textarea id="plainText" placeholder="輸入你想加密的內容,例如:秘密任務#1 😎 GO!"></textarea>
<div class="row">
<button id="encryptBtn">加密成 My Spy Code</button>
<button class="secondary" id="clearBtn">清空</button>
</div>
<div class="output" id="encryptedOutput" aria-live="polite"></div>
<div class="row">
<button class="good" id="copyCipherBtn">複製加密結果</button>
<button class="warn" id="shareBtn">分享(產生可解密連結)</button>
</div>
<div class="output" id="shareOutput"></div>
</section>

<section class="card">
<h2>2) 解密</h2>
<textarea id="cipherText" placeholder="貼上 My Spy Code(例如 ....-- .....- / -...-- ..---."></textarea>
<div class="row">
<button id="decryptBtn">解密內容</button>
</div>
<div class="output" id="decryptedOutput"></div>
</section>

<section class="card">
<h2>自訂摩斯表(與國際摩斯不同)</h2>
<div class="output" id="codebookView"></div>
</section>
</main>

<script>
const CODEBOOK = {
'0': '-----.',
'1': '.----.',
'2': '..---.',
'3': '...--.',
'4': '....-.',
'5': '.....-',
'6': '-....-',
'7': '--...-',
'8': '---..-',
'9': '----.-',
'a': '.-.-.-',
'b': '-...--',
'c': '-.-..-',
'd': '-..-.-',
'e': '..-.-.',
'f': '.--.-.'
};

// 使用者要求示例:# = ....--(優先映射)
const SPECIAL_CHAR_DIRECT = {
'#': '....--'
};

const REVERSE = Object.fromEntries(Object.entries(CODEBOOK).map(([k, v]) => [v, k]));

const EMOJI_NAME_MAP = {
'😀': 'grinning face',
'😁': 'beaming face',
'😂': 'face with tears of joy',
'🤣': 'rolling on the floor laughing',
'😊': 'smiling face with smiling eyes',
'😎': 'smiling face with sunglasses',
'❤️': 'red heart',
'👍': 'thumbs up',
'🙏': 'folded hands',
'🔥': 'fire',
'🎉': 'party popper',
'😭': 'loudly crying face',
'🥺': 'pleading face',
'🤔': 'thinking face',
'😡': 'pouting face',
'👀': 'eyes'
};

function isLikelyEmoji(char) {
return /\p{Extended_Pictographic}/u.test(char);
}

function emojiToName(char) {
if (EMOJI_NAME_MAP[char]) return EMOJI_NAME_MAP[char];
const cps = [...char].map(c => c.codePointAt(0).toString(16)).join('-');
return `emoji-${cps}`;
}

function preprocessEmojiToEnglishName(text) {
const graphemes = [...text];
return graphemes.map(ch => isLikelyEmoji(ch) ? `:${emojiToName(ch)}:` : ch).join('');
Comment on lines +189 to +190

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Segment emoji as graphemes before name preprocessing

preprocessEmojiToEnglishName uses [...text], which iterates Unicode code points rather than full grapheme clusters, so multi-codepoint emoji (for example ❤️) are split and no longer match EMOJI_NAME_MAP. This produces inconsistent placeholders and leaves artifacts (e.g., a trailing variation selector) in decrypted output, breaking the advertised emoji handling for common emoji sequences.

Useful? React with 👍 / 👎.

}

function encodeTextToSpyCode(input) {
const prepared = preprocessEmojiToEnglishName(input);
const out = [];

for (const char of [...prepared]) {
if (SPECIAL_CHAR_DIRECT[char]) {
out.push(SPECIAL_CHAR_DIRECT[char]);
continue;
}

const bytes = new TextEncoder().encode(char);
const encodedBytes = [];

for (const b of bytes) {
const hex = b.toString(16).padStart(2, '0');
const left = CODEBOOK[hex[0]];
const right = CODEBOOK[hex[1]];
encodedBytes.push(`${left} ${right}`);
}
out.push(encodedBytes.join(' / '));
}
return out.join(' | ');
}

function decodeSpyCodeToText(cipher) {
const segments = cipher.split('|').map(s => s.trim()).filter(Boolean);
const chars = [];

for (const seg of segments) {
if (seg === SPECIAL_CHAR_DIRECT['#']) {
chars.push('#');
continue;
}

const byteChunks = seg.split('/').map(s => s.trim()).filter(Boolean);
const bytes = [];

for (const chunk of byteChunks) {
const pair = chunk.split(/\s+/).filter(Boolean);
if (pair.length !== 2) throw new Error(`區塊格式錯誤:${chunk}`);
const h1 = REVERSE[pair[0]];
const h2 = REVERSE[pair[1]];
if (h1 === undefined || h2 === undefined) {
throw new Error(`找不到代碼:${chunk}`);
}
bytes.push(parseInt(h1 + h2, 16));
}

const text = new TextDecoder().decode(new Uint8Array(bytes));
chars.push(text);
}

let decoded = chars.join('');
decoded = decoded.replace(/:([a-z0-9\- ]+):/gi, '[$1]');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop rewriting literal :name: text during decode

The decoder currently applies decoded.replace(/:([a-z0-9\- ]+):/gi, '[$1]') to every message, so normal plaintext like :abc: is not round-tripped and comes back as [abc] after encode/decode. This silently corrupts user content that uses colon-delimited tokens (emoji shortcodes, labels, or markdown-like text), even when no emoji preprocessing was intended.

Useful? React with 👍 / 👎.

return decoded;
}

function renderCodebook() {
const lines = Object.entries(CODEBOOK)
.map(([hex, code]) => `${hex.toUpperCase()} => ${code}`)
.join('\n');
document.getElementById('codebookView').textContent = `${lines}\n# => ${SPECIAL_CHAR_DIRECT['#']}`;
}

function setText(id, text) {
document.getElementById(id).textContent = text || '(尚無內容)';
}

document.getElementById('encryptBtn').addEventListener('click', () => {
const plain = document.getElementById('plainText').value;
if (!plain.trim()) {
setText('encryptedOutput', '請先輸入文字');
return;
}
const cipher = encodeTextToSpyCode(plain);
setText('encryptedOutput', cipher);
document.getElementById('cipherText').value = cipher;
});

document.getElementById('decryptBtn').addEventListener('click', () => {
const cipher = document.getElementById('cipherText').value.trim();
if (!cipher) {
setText('decryptedOutput', '請先輸入加密碼');
return;
}
try {
const text = decodeSpyCodeToText(cipher);
setText('decryptedOutput', text);
} catch (err) {
setText('decryptedOutput', `解密失敗:${err.message}`);
}
});

document.getElementById('copyCipherBtn').addEventListener('click', async () => {
const cipher = document.getElementById('encryptedOutput').textContent;
if (!cipher || cipher.includes('尚無內容')) return;
await navigator.clipboard.writeText(cipher);
alert('已複製加密內容');
});

document.getElementById('shareBtn').addEventListener('click', async () => {
const cipher = document.getElementById('encryptedOutput').textContent;
if (!cipher || cipher.includes('尚無內容')) {
setText('shareOutput', '請先加密後再分享');
return;
}
const url = `${location.origin}${location.pathname}?c=${encodeURIComponent(cipher)}`;
setText('shareOutput', url);
await navigator.clipboard.writeText(url);
});

document.getElementById('clearBtn').addEventListener('click', () => {
document.getElementById('plainText').value = '';
document.getElementById('cipherText').value = '';
setText('encryptedOutput', '');
setText('decryptedOutput', '');
setText('shareOutput', '');
});

function decodeFromQueryIfExists() {
const c = new URLSearchParams(location.search).get('c');
if (!c) return;
document.getElementById('cipherText').value = c;
setText('encryptedOutput', c);
try {
const decoded = decodeSpyCodeToText(c);
setText('decryptedOutput', `${decoded}\n\n(此內容由分享連結自動解密)`);
} catch (err) {
setText('decryptedOutput', `自動解密失敗:${err.message}`);
}
}

setText('encryptedOutput', '');
setText('decryptedOutput', '');
setText('shareOutput', '');
renderCodebook();
decodeFromQueryIfExists();
</script>
</body>
</html>
Loading