feat: Add script to generate AWS CCP practice exam Anki deck and initial deck file.
This commit is contained in:
BIN
practice-exam/aws-ccp-all-practice-exams.apkg
Normal file
BIN
practice-exam/aws-ccp-all-practice-exams.apkg
Normal file
Binary file not shown.
781
practice-exam/generate_anki_deck.py
Normal file
781
practice-exam/generate_anki_deck.py
Normal file
@@ -0,0 +1,781 @@
|
||||
"""
|
||||
AWS Certified Cloud Practitioner Practice Exam to Anki Deck Converter
|
||||
|
||||
This script parses practice exam markdown files and generates Anki decks.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import glob
|
||||
import random
|
||||
import genanki
|
||||
|
||||
|
||||
def generate_unique_id():
|
||||
"""Generate a unique ID for Anki models and decks."""
|
||||
return random.randrange(1 << 30, 1 << 31)
|
||||
|
||||
|
||||
# Define the Anki card model with interactive quiz functionality
|
||||
AWS_MODEL = genanki.Model(
|
||||
generate_unique_id(),
|
||||
'AWS Practice Exam Interactive Model',
|
||||
fields=[
|
||||
{'name': 'Question'},
|
||||
{'name': 'OptionsHTML'},
|
||||
{'name': 'Answer'},
|
||||
{'name': 'Source'},
|
||||
{'name': 'IsMultiple'}, # "true" or "false" - whether multiple answers are expected
|
||||
],
|
||||
templates=[
|
||||
{
|
||||
'name': 'Interactive Quiz Card',
|
||||
'qfmt': '''
|
||||
<div class="question">{{Question}}</div>
|
||||
<hr>
|
||||
<div class="hint-text" id="hintText"></div>
|
||||
<div class="options-container" id="options">{{OptionsHTML}}</div>
|
||||
<div class="button-container">
|
||||
<button id="checkBtn" onclick="checkAnswer()">Antwort prüfen ✓</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var correctAnswer = "{{Answer}}";
|
||||
var isMultiple = "{{IsMultiple}}" === "true";
|
||||
|
||||
// Toggle option when clicking on the entire box
|
||||
function toggleOption(event, element) {
|
||||
var input = element.querySelector('input');
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = !input.checked;
|
||||
} else {
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Set hint text based on number of answers
|
||||
(function() {
|
||||
var hintDiv = document.getElementById('hintText');
|
||||
if (isMultiple) {
|
||||
var numAnswers = correctAnswer.split(',').length;
|
||||
hintDiv.innerHTML = '💡 Wähle ' + numAnswers + ' Antworten aus';
|
||||
hintDiv.style.display = 'block';
|
||||
}
|
||||
})();
|
||||
|
||||
function getSelectedAnswers() {
|
||||
var selected = [];
|
||||
var inputs = document.querySelectorAll('input[name="answer"]:checked');
|
||||
inputs.forEach(function(input) {
|
||||
selected.push(input.value);
|
||||
});
|
||||
return selected.sort().join(", ");
|
||||
}
|
||||
|
||||
function checkAnswer() {
|
||||
var selected = getSelectedAnswers();
|
||||
|
||||
if (selected === "") {
|
||||
alert("Bitte wähle mindestens eine Antwort aus!");
|
||||
return;
|
||||
}
|
||||
|
||||
// Store the result for the back side
|
||||
var normalizedCorrect = correctAnswer.replace(/\\s/g, "").toUpperCase().split(",").sort().join(",");
|
||||
var normalizedSelected = selected.replace(/\\s/g, "").toUpperCase().split(",").sort().join(",");
|
||||
var isCorrect = normalizedSelected === normalizedCorrect;
|
||||
|
||||
// Store result in sessionStorage for back side
|
||||
sessionStorage.setItem('lastAnswer', selected);
|
||||
sessionStorage.setItem('wasCorrect', isCorrect ? 'true' : 'false');
|
||||
|
||||
// Trigger Anki to show the answer
|
||||
if (typeof pycmd !== 'undefined') {
|
||||
pycmd('ans');
|
||||
} else if (typeof AnkiDroidJS !== 'undefined') {
|
||||
showAnswer();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
''',
|
||||
'afmt': '''
|
||||
<div class="question">{{Question}}</div>
|
||||
<hr>
|
||||
<div class="options-container answered" id="options">{{OptionsHTML}}</div>
|
||||
|
||||
<div id="result" class="result"></div>
|
||||
<div class="correct-answer">
|
||||
<strong>Richtige Antwort:</strong> {{Answer}}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
<div class="source">
|
||||
<em>Quelle: {{Source}}</em>
|
||||
</div>
|
||||
|
||||
<div class="rating-container">
|
||||
<p class="rating-hint" id="ratingHint">Bewerte deine Antwort:</p>
|
||||
<div class="rating-buttons">
|
||||
<button class="rating-btn again" onclick="answerCard(1)">Nochmal<br><span class="rating-sub"><1min</span></button>
|
||||
<button class="rating-btn hard" onclick="answerCard(2)">Schwer<br><span class="rating-sub"><6min</span></button>
|
||||
<button class="rating-btn good" onclick="answerCard(3)">Gut<br><span class="rating-sub"><10min</span></button>
|
||||
<button class="rating-btn easy" onclick="answerCard(4)">Einfach<br><span class="rating-sub">4 Tage</span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var correctAnswer = "{{Answer}}";
|
||||
|
||||
// Cross-platform answer function for desktop Anki and AnkiDroid
|
||||
function answerCard(ease) {
|
||||
// AnkiDroid uses direct functions
|
||||
if (typeof buttonAnswerEase1 !== 'undefined') {
|
||||
switch(ease) {
|
||||
case 1: buttonAnswerEase1(); break;
|
||||
case 2: buttonAnswerEase2(); break;
|
||||
case 3: buttonAnswerEase3(); break;
|
||||
case 4: buttonAnswerEase4(); break;
|
||||
}
|
||||
} else if (typeof AnkiDroidJS !== 'undefined') {
|
||||
// Fallback for older AnkiDroid versions
|
||||
AnkiDroidJS.ankiAnswerCard(ease);
|
||||
} else if (typeof pycmd !== 'undefined') {
|
||||
// Desktop Anki
|
||||
pycmd('ease' + ease);
|
||||
}
|
||||
}
|
||||
|
||||
(function() {
|
||||
var selected = sessionStorage.getItem('lastAnswer') || '';
|
||||
var wasCorrect = sessionStorage.getItem('wasCorrect') === 'true';
|
||||
var resultDiv = document.getElementById('result');
|
||||
var ratingHint = document.getElementById('ratingHint');
|
||||
|
||||
// Highlight options
|
||||
var options = document.querySelectorAll('.option-item');
|
||||
var correctLetters = correctAnswer.replace(/\\s/g, "").toUpperCase().split(",");
|
||||
var selectedLetters = selected.replace(/\\s/g, "").toUpperCase().split(",");
|
||||
|
||||
options.forEach(function(opt) {
|
||||
var input = opt.querySelector('input');
|
||||
if (input) {
|
||||
var letter = input.value.toUpperCase();
|
||||
input.disabled = true;
|
||||
|
||||
if (correctLetters.includes(letter)) {
|
||||
opt.classList.add('correct-option');
|
||||
input.checked = true;
|
||||
}
|
||||
if (selectedLetters.includes(letter) && !correctLetters.includes(letter)) {
|
||||
opt.classList.add('incorrect-option');
|
||||
input.checked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Show result
|
||||
if (selected) {
|
||||
if (wasCorrect) {
|
||||
resultDiv.innerHTML = "✅ Richtig!";
|
||||
resultDiv.className = "result correct";
|
||||
ratingHint.innerHTML = "🎉 Super! Wie gut hast du es gewusst?";
|
||||
} else {
|
||||
resultDiv.innerHTML = "❌ Falsch! Deine Antwort: " + selected;
|
||||
resultDiv.className = "result incorrect";
|
||||
ratingHint.innerHTML = "📚 Für nächstes Mal merken:";
|
||||
}
|
||||
}
|
||||
|
||||
// Clear storage
|
||||
sessionStorage.removeItem('lastAnswer');
|
||||
sessionStorage.removeItem('wasCorrect');
|
||||
})();
|
||||
</script>
|
||||
''',
|
||||
},
|
||||
],
|
||||
css='''
|
||||
.card {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
||||
font-size: 16px;
|
||||
text-align: left;
|
||||
color: #333;
|
||||
background-color: #f8f9fa;
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.question {
|
||||
font-weight: 600;
|
||||
margin-bottom: 20px;
|
||||
font-size: 18px;
|
||||
color: #232f3e;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.options-container {
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
.option-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin: 10px 0;
|
||||
padding: 12px 15px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
border: 2px solid #e0e0e0;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.option-item:hover {
|
||||
border-color: #ff9900;
|
||||
background-color: #fff8f0;
|
||||
}
|
||||
|
||||
.options-container.answered .option-item:hover {
|
||||
border-color: inherit;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.option-item input {
|
||||
margin-right: 12px;
|
||||
margin-top: 3px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
accent-color: #ff9900;
|
||||
}
|
||||
|
||||
.option-item label {
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.option-item.correct-option {
|
||||
border-color: #28a745;
|
||||
background-color: #d4edda;
|
||||
}
|
||||
|
||||
.option-item.incorrect-option {
|
||||
border-color: #dc3545;
|
||||
background-color: #f8d7da;
|
||||
}
|
||||
|
||||
.hint-text {
|
||||
display: none;
|
||||
background-color: #e3f2fd;
|
||||
color: #1565c0;
|
||||
padding: 10px 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.button-container {
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 30px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#checkBtn {
|
||||
background: linear-gradient(135deg, #ff9900, #ffad33);
|
||||
color: #232f3e;
|
||||
}
|
||||
|
||||
#checkBtn:hover {
|
||||
background: linear-gradient(135deg, #ec8b00, #ff9900);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(255, 153, 0, 0.4);
|
||||
}
|
||||
|
||||
.result {
|
||||
padding: 15px 20px;
|
||||
border-radius: 8px;
|
||||
margin: 15px 0;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.result.correct {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 2px solid #28a745;
|
||||
}
|
||||
|
||||
.result.incorrect {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 2px solid #dc3545;
|
||||
}
|
||||
|
||||
.correct-answer {
|
||||
padding: 15px 20px;
|
||||
background-color: #e8f5e9;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #28a745;
|
||||
margin: 15px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.source {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.rating-container {
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background-color: #fff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.rating-hint {
|
||||
text-align: center;
|
||||
font-size: 16px;
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.rating-buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.rating-btn {
|
||||
padding: 15px 20px;
|
||||
min-width: 80px;
|
||||
font-size: 14px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.rating-btn .rating-sub {
|
||||
font-size: 11px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.rating-btn.again {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rating-btn.again:hover {
|
||||
background: #c82333;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.rating-btn.hard {
|
||||
background: #fd7e14;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rating-btn.hard:hover {
|
||||
background: #e96b02;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.rating-btn.good {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rating-btn.good:hover {
|
||||
background: #218838;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.rating-btn.easy {
|
||||
background: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.rating-btn.easy:hover {
|
||||
background: #138496;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #dee2e6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* Hide custom rating buttons on AnkiDroid/mobile (uses built-in buttons) */
|
||||
.mobile .rating-container,
|
||||
.android .rating-container {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Dark Mode Styles */
|
||||
.night_mode .card {
|
||||
background-color: #1e1e2e;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode .question {
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode .option-item {
|
||||
background-color: #313244;
|
||||
border-color: #45475a;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode .option-item:hover {
|
||||
border-color: #fab387;
|
||||
background-color: #45475a;
|
||||
}
|
||||
|
||||
.night_mode .option-item label {
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode .option-item.correct-option {
|
||||
border-color: #a6e3a1;
|
||||
background-color: rgba(166, 227, 161, 0.2);
|
||||
}
|
||||
|
||||
.night_mode .option-item.incorrect-option {
|
||||
border-color: #f38ba8;
|
||||
background-color: rgba(243, 139, 168, 0.2);
|
||||
}
|
||||
|
||||
.night_mode .hint-text {
|
||||
background-color: rgba(137, 180, 250, 0.2);
|
||||
color: #89b4fa;
|
||||
}
|
||||
|
||||
.night_mode #checkBtn {
|
||||
background: linear-gradient(135deg, #fab387, #f9e2af);
|
||||
color: #1e1e2e;
|
||||
}
|
||||
|
||||
.night_mode #checkBtn:hover {
|
||||
background: linear-gradient(135deg, #f9e2af, #fab387);
|
||||
box-shadow: 0 4px 12px rgba(250, 179, 135, 0.4);
|
||||
}
|
||||
|
||||
.night_mode .result.correct {
|
||||
background-color: rgba(166, 227, 161, 0.2);
|
||||
color: #a6e3a1;
|
||||
border-color: #a6e3a1;
|
||||
}
|
||||
|
||||
.night_mode .result.incorrect {
|
||||
background-color: rgba(243, 139, 168, 0.2);
|
||||
color: #f38ba8;
|
||||
border-color: #f38ba8;
|
||||
}
|
||||
|
||||
.night_mode .correct-answer {
|
||||
background-color: rgba(166, 227, 161, 0.15);
|
||||
border-left-color: #a6e3a1;
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode .source {
|
||||
color: #a6adc8;
|
||||
}
|
||||
|
||||
.night_mode .rating-container {
|
||||
background-color: #313244;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.night_mode .rating-hint {
|
||||
color: #cdd6f4;
|
||||
}
|
||||
|
||||
.night_mode hr {
|
||||
border-top-color: #45475a;
|
||||
}
|
||||
'''
|
||||
)
|
||||
|
||||
|
||||
def parse_markdown_file(filepath: str) -> list[dict]:
|
||||
"""
|
||||
Parse a practice exam markdown file and extract questions.
|
||||
|
||||
Args:
|
||||
filepath: Path to the markdown file
|
||||
|
||||
Returns:
|
||||
List of dictionaries containing question data
|
||||
"""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
questions = []
|
||||
|
||||
# Pattern to match each question block
|
||||
# Questions start with a number followed by a period
|
||||
question_pattern = r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)'
|
||||
|
||||
# Split content by question numbers at the start of lines
|
||||
question_blocks = re.split(r'\n(?=\d+\.\s)', content)
|
||||
|
||||
for block in question_blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
|
||||
# Match question number and text
|
||||
q_match = re.match(r'(\d+)\.\s+(.+?)(?=\n\s*-\s+A\.)', block, re.DOTALL)
|
||||
if not q_match:
|
||||
continue
|
||||
|
||||
question_num = q_match.group(1)
|
||||
question_text = q_match.group(2).strip()
|
||||
|
||||
# Extract options (A, B, C, D, E)
|
||||
options = []
|
||||
option_pattern = r'-\s+([A-E])\.(.+?)(?=(?:\n\s*-\s+[A-E]\.)|\n\s*<details|\Z)'
|
||||
option_matches = re.findall(option_pattern, block, re.DOTALL)
|
||||
|
||||
for letter, text in option_matches:
|
||||
options.append(f"{letter}. {text.strip()}")
|
||||
|
||||
# Extract answer from <details> block
|
||||
answer_pattern = r'Correct answer:\s*([A-E](?:,\s*[A-E])*)'
|
||||
answer_match = re.search(answer_pattern, block)
|
||||
|
||||
if answer_match and options:
|
||||
answer = answer_match.group(1).strip()
|
||||
|
||||
# Check if multiple answers are expected
|
||||
is_multiple = ',' in answer
|
||||
|
||||
questions.append({
|
||||
'number': question_num,
|
||||
'question': question_text,
|
||||
'options': options,
|
||||
'answer': answer,
|
||||
'is_multiple': is_multiple
|
||||
})
|
||||
|
||||
return questions
|
||||
|
||||
|
||||
def create_anki_deck(questions: list[dict], deck_name: str, source_file: str) -> genanki.Deck:
|
||||
"""
|
||||
Create an Anki deck from parsed questions.
|
||||
|
||||
Args:
|
||||
questions: List of question dictionaries
|
||||
deck_name: Name for the Anki deck
|
||||
source_file: Source file name for reference
|
||||
|
||||
Returns:
|
||||
genanki.Deck object
|
||||
"""
|
||||
deck = genanki.Deck(generate_unique_id(), deck_name)
|
||||
|
||||
for q in questions:
|
||||
# Format options as interactive HTML with checkboxes or radio buttons
|
||||
is_multiple = q.get('is_multiple', False)
|
||||
input_type = 'checkbox' if is_multiple else 'radio'
|
||||
|
||||
options_html_parts = []
|
||||
for opt in q['options']:
|
||||
letter = opt[0] # First character is the option letter
|
||||
text = opt[3:] # Skip "X. " prefix
|
||||
option_html = f'''<div class="option-item" onclick="toggleOption(event, this)">
|
||||
<input type="{input_type}" name="answer" value="{letter}" id="opt_{letter}" onclick="event.stopPropagation()">
|
||||
<label for="opt_{letter}"><strong>{letter}.</strong> {text}</label>
|
||||
</div>'''
|
||||
options_html_parts.append(option_html)
|
||||
|
||||
options_html = '\n'.join(options_html_parts)
|
||||
|
||||
note = genanki.Note(
|
||||
model=AWS_MODEL,
|
||||
fields=[
|
||||
q['question'],
|
||||
options_html,
|
||||
q['answer'],
|
||||
f"{source_file} - Q{q['number']}",
|
||||
"true" if is_multiple else "false"
|
||||
]
|
||||
)
|
||||
deck.add_note(note)
|
||||
|
||||
return deck
|
||||
|
||||
|
||||
def process_single_file(filepath: str, output_dir: str = None):
|
||||
"""
|
||||
Process a single practice exam file and create an Anki deck.
|
||||
|
||||
Args:
|
||||
filepath: Path to the markdown file
|
||||
output_dir: Output directory for the .apkg file (defaults to same as input)
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = os.path.dirname(filepath)
|
||||
|
||||
filename = os.path.basename(filepath)
|
||||
deck_name = f"AWS CCP - {filename.replace('.md', '').replace('-', ' ').title()}"
|
||||
|
||||
print(f"Processing: {filename}")
|
||||
questions = parse_markdown_file(filepath)
|
||||
print(f" Found {len(questions)} questions")
|
||||
|
||||
if questions:
|
||||
deck = create_anki_deck(questions, deck_name, filename)
|
||||
output_path = os.path.join(output_dir, filename.replace('.md', '.apkg'))
|
||||
genanki.Package(deck).write_to_file(output_path)
|
||||
print(f" Created: {output_path}")
|
||||
return deck
|
||||
else:
|
||||
print(f" No questions found in {filename}")
|
||||
return None
|
||||
|
||||
|
||||
def process_all_files(directory: str, output_dir: str = None, combined: bool = True):
|
||||
"""
|
||||
Process all practice exam files in a directory.
|
||||
|
||||
Args:
|
||||
directory: Directory containing practice exam markdown files
|
||||
output_dir: Output directory for .apkg files
|
||||
combined: If True, create a single combined deck; if False, create separate decks
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = directory
|
||||
|
||||
pattern = os.path.join(directory, 'practice-exam-*.md')
|
||||
files = sorted(glob.glob(pattern))
|
||||
|
||||
if not files:
|
||||
print(f"No practice exam files found matching: {pattern}")
|
||||
return
|
||||
|
||||
print(f"Found {len(files)} practice exam files")
|
||||
|
||||
if combined:
|
||||
# Create a combined deck
|
||||
all_questions = []
|
||||
for filepath in files:
|
||||
filename = os.path.basename(filepath)
|
||||
print(f"Processing: {filename}")
|
||||
questions = parse_markdown_file(filepath)
|
||||
|
||||
# Add source file info to each question
|
||||
for q in questions:
|
||||
q['source_file'] = filename
|
||||
|
||||
all_questions.extend(questions)
|
||||
print(f" Found {len(questions)} questions")
|
||||
|
||||
print(f"\nTotal questions: {len(all_questions)}")
|
||||
|
||||
if all_questions:
|
||||
deck = genanki.Deck(
|
||||
generate_unique_id(),
|
||||
'AWS Certified Cloud Practitioner - All Practice Exams'
|
||||
)
|
||||
|
||||
for q in all_questions:
|
||||
# Format options as interactive HTML with checkboxes or radio buttons
|
||||
is_multiple = q.get('is_multiple', False)
|
||||
input_type = 'checkbox' if is_multiple else 'radio'
|
||||
|
||||
options_html_parts = []
|
||||
for opt in q['options']:
|
||||
letter = opt[0] # First character is the option letter
|
||||
text = opt[3:] # Skip "X. " prefix
|
||||
option_html = f'''<div class="option-item" onclick="toggleOption(event, this)">
|
||||
<input type="{input_type}" name="answer" value="{letter}" id="opt_{letter}" onclick="event.stopPropagation()">
|
||||
<label for="opt_{letter}"><strong>{letter}.</strong> {text}</label>
|
||||
</div>'''
|
||||
options_html_parts.append(option_html)
|
||||
|
||||
options_html = '\n'.join(options_html_parts)
|
||||
|
||||
note = genanki.Note(
|
||||
model=AWS_MODEL,
|
||||
fields=[
|
||||
q['question'],
|
||||
options_html,
|
||||
q['answer'],
|
||||
f"{q['source_file']} - Q{q['number']}",
|
||||
"true" if is_multiple else "false"
|
||||
]
|
||||
)
|
||||
deck.add_note(note)
|
||||
|
||||
output_path = os.path.join(output_dir, 'aws-ccp-all-practice-exams.apkg')
|
||||
genanki.Package(deck).write_to_file(output_path)
|
||||
print(f"\nCreated combined deck: {output_path}")
|
||||
else:
|
||||
# Create separate decks for each file
|
||||
for filepath in files:
|
||||
process_single_file(filepath, output_dir)
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Convert AWS Practice Exam markdown files to Anki decks'
|
||||
)
|
||||
parser.add_argument(
|
||||
'input',
|
||||
nargs='?',
|
||||
default='.',
|
||||
help='Input file or directory (default: current directory)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-o', '--output',
|
||||
help='Output directory for .apkg files (default: same as input)'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-s', '--separate',
|
||||
action='store_true',
|
||||
help='Create separate decks for each file instead of a combined deck'
|
||||
)
|
||||
parser.add_argument(
|
||||
'-f', '--file',
|
||||
help='Process a single specific file'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.file:
|
||||
# Process single file
|
||||
process_single_file(args.file, args.output)
|
||||
elif os.path.isfile(args.input):
|
||||
# Input is a file
|
||||
process_single_file(args.input, args.output)
|
||||
else:
|
||||
# Input is a directory
|
||||
process_all_files(args.input, args.output, combined=not args.separate)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user