Created
August 20, 2025 21:04
-
-
Save z1lc/f1d572307b99d7d4783ec45538761bf5 to your computer and use it in GitHub Desktop.
easily create a set of Cloze deletions from a newline-seperated list
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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