Skip to content

Instantly share code, notes, and snippets.

@z1lc
Created August 20, 2025 21:04
Show Gist options
  • Select an option

  • Save z1lc/f1d572307b99d7d4783ec45538761bf5 to your computer and use it in GitHub Desktop.

Select an option

Save z1lc/f1d572307b99d7d4783ec45538761bf5 to your computer and use it in GitHub Desktop.
easily create a set of Cloze deletions from a newline-seperated list
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anki Cloze Deletion Helper</title>
<!-- Tailwind CSS for styling -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Google Fonts for a clean, modern look -->
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
/* Applying the Inter font to the body */
body {
font-family: 'Inter', sans-serif;
}
/* Custom styling for keyboard shortcut keys */
kbd {
box-shadow: 0 2px 0px rgba(0,0,0,0.2);
position: relative;
top: -1px;
}
/* Style for the output container to have a scrollbar if needed */
#output-container {
max-height: 50vh;
overflow-y: auto;
}
/* Simple fade-in animation for new elements */
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
</style>
</head>
<body class="bg-gray-100 text-gray-800 flex items-center justify-center min-h-screen p-4">
<div class="w-full max-w-3xl mx-auto">
<div class="bg-white rounded-xl shadow-lg p-6 md:p-8">
<div class="text-center">
<h1 class="text-2xl md:text-3xl font-bold text-gray-900 mb-2">Anki Cloze Processor</h1>
<p class="text-gray-600 mb-4">
Paste your notes below. Each line will become a separate card.
</p>
<div class="flex justify-center space-x-4 text-sm text-gray-500 mb-6">
<span><kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">Cmd/Ctrl+Shift+C</kbd> to cloze</span>
<span><kbd class="px-2 py-1.5 text-xs font-semibold text-gray-800 bg-gray-100 border border-gray-200 rounded-lg">Cmd/Ctrl+Enter</kbd> for next</span>
</div>
</div>
<!-- Chunk progress indicator -->
<div id="progress-indicator" class="text-center text-sm text-gray-500 mb-2 h-5"></div>
<!-- The main text area for user input -->
<textarea
id="editor"
class="w-full h-48 p-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition duration-150 ease-in-out text-base"
placeholder="Paste your text here. Each line will be a new chunk to process."
></textarea>
</div>
<!-- Output area for processed chunks -->
<div id="output-area" class="w-full mt-6">
<h2 id="output-title" class="text-xl font-semibold text-gray-700 mb-4 text-center hidden">Processed Cards</h2>
<div id="output-container" class="space-y-3">
<!-- Processed chunks will be inserted here by JavaScript -->
</div>
<!-- "Copy All" button container -->
<div id="copy-all-container" class="text-center mt-4 hidden">
<button id="copy-all-btn" class="w-full md:w-auto px-6 py-2 bg-green-600 text-white font-semibold rounded-lg hover:bg-green-700 transition-colors">
Copy All
</button>
</div>
</div>
</div>
<script>
// --- DOM Element References ---
const editor = document.getElementById('editor');
const progressIndicator = document.getElementById('progress-indicator');
const outputContainer = document.getElementById('output-container');
const outputTitle = document.getElementById('output-title');
const copyAllContainer = document.getElementById('copy-all-container');
const copyAllBtn = document.getElementById('copy-all-btn');
// --- State Management ---
let chunks = [];
let currentChunkIndex = -1; // -1 indicates no session is active
// --- Core Functions ---
/**
* Loads the current chunk into the editor and updates the progress indicator.
*/
function loadCurrentChunk() {
if (currentChunkIndex >= 0 && currentChunkIndex < chunks.length) {
editor.value = chunks[currentChunkIndex];
progressIndicator.textContent = `Processing card ${currentChunkIndex + 1} of ${chunks.length}`;
editor.disabled = false;
editor.focus();
} else {
editor.value = 'All cards processed! 🎉';
progressIndicator.textContent = 'Done!';
editor.disabled = true;
chunks = [];
currentChunkIndex = -1; // Reset the session
}
}
/**
* Adds the processed text to the output area at the top.
* @param {string} text - The processed text to display.
*/
function addResultToOutput(text) {
outputTitle.classList.remove('hidden');
copyAllContainer.classList.remove('hidden');
const newOutput = document.createElement('div');
newOutput.className = 'bg-white p-4 rounded-lg shadow-sm flex justify-between items-center animate-fade-in';
const textElement = document.createElement('pre');
textElement.textContent = text;
textElement.className = 'text-gray-800 whitespace-pre-wrap flex-grow mr-4';
const copyButton = document.createElement('button');
copyButton.textContent = 'Copy';
copyButton.className = 'flex-shrink-0 ml-4 px-3 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors text-sm';
copyButton.onclick = () => {
copyToClipboard(text, copyButton);
};
newOutput.appendChild(textElement);
newOutput.appendChild(copyButton);
outputContainer.insertBefore(newOutput, outputContainer.firstChild);
}
/**
* Copies provided text to the clipboard and provides feedback on a button.
* @param {string} text - The text to copy.
* @param {HTMLElement} buttonElement - The button that triggered the copy action.
*/
function copyToClipboard(text, buttonElement) {
const textarea = document.createElement('textarea');
textarea.value = text;
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
buttonElement.textContent = 'Copied!';
setTimeout(() => { buttonElement.textContent = buttonElement.id === 'copy-all-btn' ? 'Copy All' : 'Copy'; }, 2000);
} catch (err) {
console.error('Failed to copy text: ', err);
buttonElement.textContent = 'Error';
}
document.body.removeChild(textarea);
}
// --- Event Listeners ---
/**
* Handles pasting text. The first paste starts the session.
* Subsequent pastes only affect the current text in the editor.
*/
editor.addEventListener('paste', function(e) {
e.preventDefault();
const pastedText = (e.clipboardData || window.clipboardData).getData('text');
// If no session is active (currentChunkIndex is -1), this is the initial paste.
if (currentChunkIndex === -1) {
chunks = pastedText.split('\n').filter(line => line.trim() !== '');
if (chunks.length > 0) {
currentChunkIndex = 0;
outputContainer.innerHTML = '';
outputTitle.classList.add('hidden');
copyAllContainer.classList.add('hidden');
loadCurrentChunk();
}
} else {
// A session is active, so just paste into the current editor view.
const start = editor.selectionStart;
const end = editor.selectionEnd;
const text = editor.value;
editor.value = text.substring(0, start) + pastedText + text.substring(end);
editor.selectionStart = editor.selectionEnd = start + pastedText.length;
}
});
/**
* Handles keyboard shortcuts.
*/
editor.addEventListener('keydown', function(e) {
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
const isModifier = isMac ? e.metaKey : e.ctrlKey;
// --- Shortcut: Next Chunk (Cmd/Ctrl + Enter) ---
if (isModifier && e.key === 'Enter') {
e.preventDefault();
if (currentChunkIndex >= 0 && currentChunkIndex < chunks.length) {
addResultToOutput(editor.value);
currentChunkIndex++;
loadCurrentChunk();
}
}
// --- Shortcut: Create Cloze (Cmd/Ctrl + Shift + C) ---
if (isModifier && e.shiftKey && e.key.toLowerCase() === 'c') {
e.preventDefault();
const text = editor.value;
const selectionStart = editor.selectionStart;
const selectionEnd = editor.selectionEnd;
const selectedText = text.substring(selectionStart, selectionEnd);
if (!selectedText) return;
let maxCloze = 0;
const clozeRegex = /{{c(\d+)::/g;
let match;
while ((match = clozeRegex.exec(text)) !== null) {
const num = parseInt(match[1], 10);
if (num > maxCloze) maxCloze = num;
}
const nextClozeNum = maxCloze + 1;
const clozeText = `{{c${nextClozeNum}::${selectedText}}}`;
editor.value = text.substring(0, selectionStart) + clozeText + text.substring(selectionEnd);
const newCursorPosition = selectionStart + clozeText.length;
editor.focus();
editor.setSelectionRange(newCursorPosition, newCursorPosition);
}
});
/**
* Handles the "Copy All" button click.
*/
copyAllBtn.addEventListener('click', function() {
const allOutputElements = outputContainer.querySelectorAll('pre');
const allText = Array.from(allOutputElements).map(el => el.textContent).reverse().join('\n');
if (allText) {
copyToClipboard(allText, copyAllBtn);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment