1<?php 2/** 3 * DokuWiki Plugin flashcards (Syntax Component) 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * author: Jakub Pelc 7 */ 8 9// Must be run within DokuWiki 10if (!defined('DOKU_INC')) die(); 11 12class syntax_plugin_flashcards extends DokuWiki_Syntax_Plugin { 13 14 public function getType() { 15 return 'container'; 16 } 17 18 public function getPType() { 19 return 'block'; 20 } 21 22 public function getAllowedTypes() { 23 return ['container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs']; 24 } 25 26 public function getSort() { 27 return 158; 28 } 29 30 public function connectTo($mode) { 31 $this->Lexer->addSpecialPattern('<flashcards.*?>.*?</flashcards>', $mode, 'plugin_flashcards'); 32 } 33 34 public function handle($match, $state, $pos, Doku_Handler $handler) { 35 $match = trim(substr($match, 12, -13)); // Strip <flashcards> tags 36 37 // Parse optional attributes 38 preg_match('/heading="(.*?)"/', $match, $headingMatch); 39 preg_match('/subtext="(.*?)"/', $match, $subtextMatch); 40 preg_match('/skiptext="(.*?)"/', $match, $skipTextMatch); 41 preg_match('/nexttext="(.*?)"/', $match, $nextTextMatch); 42 preg_match('/defaultnum="(\d+)"/', $match, $defaultNumMatch); 43 44 $heading = $headingMatch[1] ?? 'Flashcard Quiz'; 45 $subtext = $subtextMatch[1] ?? 'Answer the following questions:'; 46 $skipText = $skipTextMatch[1] ?? 'Skip'; 47 $nextText = $nextTextMatch[1] ?? 'Next'; 48 $defaultNum = $defaultNumMatch[1] ?? 5; // Default to 5 if not provided 49 50 // Parse the content within the <questions></questions> block 51 if (preg_match('/<questions>(.*?)<\/questions>/s', $match, $contentMatch)) { 52 $content = trim($contentMatch[1]); 53 } else { 54 return [ 55 'heading' => $heading, 56 'subtext' => $subtext, 57 'skipText' => $skipText, 58 'nextText' => $nextText, 59 'defaultNum' => $defaultNum, 60 'questions' => [], // No valid content found 61 ]; 62 } 63 64 // Parse questions and answers using --- as delimiter 65 $questions = []; 66 foreach (preg_split('/---\n/', $content) as $block) { 67 $lines = array_filter(explode("\n", trim($block))); // Filter empty lines 68 $question = array_shift($lines); 69 70 if (empty($question)) { 71 continue; // Skip empty question blocks 72 } 73 74 $answers = []; 75 $correctAnswerIndex = null; 76 77 foreach ($lines as $index => $line) { 78 $line = trim($line); 79 80 if (strpos($line, '*') !== false) { 81 $correctAnswerIndex = $index; 82 $line = str_replace('*', '', $line); // Remove the correct marker 83 } 84 85 $answers[] = trim($line, "- "); 86 } 87 88 if (empty($answers) || $correctAnswerIndex === null) { 89 continue; // Skip questions without valid answers or no correct answer 90 } 91 92 $questions[] = [ 93 'question' => trim($question), 94 'answers' => $answers, 95 'correct' => $correctAnswerIndex, 96 ]; 97 } 98 99 return [ 100 'heading' => $heading, 101 'subtext' => $subtext, 102 'skipText' => $skipText, 103 'nextText' => $nextText, 104 'defaultNum' => $defaultNum, 105 'questions' => $questions, 106 ]; 107 } 108 109 public function render($mode, Doku_Renderer $renderer, $data) { 110 if ($mode !== 'xhtml') return false; 111 112 $heading = htmlspecialchars($data['heading']); 113 $subtext = htmlspecialchars($data['subtext']); 114 $skipText = htmlspecialchars($data['skipText']); 115 $nextText = htmlspecialchars($data['nextText']); 116 $defaultNum = htmlspecialchars($data['defaultNum']); 117 $questions = json_encode($data['questions'], JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); 118 119 $renderer->doc .= "<div id='flashcards-container'> 120 <h1>{$heading}</h1> 121 <p>{$subtext}</p> 122 <div id='app-container'> 123 <div id='welcome-screen'> 124 <input type='number' id='question-count-input' placeholder='Enter number of questions' min='1' value='{$defaultNum}'> 125 <button class='button' id='start-button'>Start Test</button> 126 </div> 127 <div id='test-container' style='display: none;'> 128 <div class='question'> 129 <p id='question-text'></p> 130 <div class='answers' id='answers-container'></div> 131 <button id='skip-button' class='button'>{$skipText}</button> 132 <button id='next-button' class='button' style='display: none;'>{$nextText}</button> 133 </div> 134 </div> 135 <div id='summary-container' style='display: none;'> 136 <div id='detailed-summary'></div> 137 </div> 138 </div> 139 </div> 140 <link rel='stylesheet' href='" . DOKU_BASE . "lib/plugins/flashcards/style.css'> 141 <script> 142 const originalQuestions = {$questions}; 143 </script> 144 <script src='" . DOKU_BASE . "lib/plugins/flashcards/script.js'></script>"; 145 146 return true; 147 } 148} 149