public function convert(BaseQuestionType $question, $interactionIdentifier, $interactionLabel) { /** @var tokenhighlight $question */ // Grab those `template` and convert those highlights to <hottext> $html = new SimpleHtmlDom(); $html->load($question->get_template()); $tokens = $html->find('span.lrn_token'); $indexIdentifierMap = []; foreach ($tokens as $key => &$span) { $span->outertext = '<hottext identifier="TOKEN_' . intval($key) . '">' . $span->innertext . '</hottext>'; $indexIdentifierMap[$key] = 'TOKEN_' . intval($key); } $htmlContent = $html->save(); $contentComponents = QtiMarshallerUtil::unmarshallElement($htmlContent); $contentCollection = ContentCollectionBuilder::buildBlockStaticCollectionContent($contentComponents); // Build the interaction $interaction = new HottextInteraction($interactionIdentifier, $contentCollection); $interaction->setLabel($interactionLabel); $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus())); // Learnosity does not enforce number of choices, thus using default such the min choice would be 1 // and max would be the `max_selection` if set, otherwise use token count $interaction->setMinChoices(1); $interaction->setMaxChoices(is_int($question->get_max_selection()) ? $question->get_max_selection() : count($tokens)); // Build validation $builder = new TokenhighlightValidationBuilder($indexIdentifierMap); list($responseDeclaration, $responseProcessing) = $builder->buildValidation($interaction->getResponseIdentifier(), $question->get_validation()); return [$interaction, $responseDeclaration, $responseProcessing]; }
public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel) { /** @var clozetext $question */ $question = $questionType; // Extra text that can't be mapped since we are in textEntryInteraction which does not have prompt $this->extraContent = $question->get_stimulus(); // Replace {{ response }} with `textEntryInteraction` elements $maxLength = !is_null($question->get_max_length()) ? intval($question->get_max_length()) : 15; // Set default to `15` if not set $index = 0; $template = preg_replace_callback('/{{response}}/', function ($match) use(&$index, $interactionIdentifier, $interactionLabel, $maxLength) { $interaction = new TextEntryInteraction($interactionIdentifier . '_' . $index); $interaction->setLabel($interactionLabel); $interaction->setExpectedLength($maxLength); $index++; $replacement = QtiMarshallerUtil::marshall($interaction); return $replacement; }, $question->get_template()); // Wrap this interaction in a block since our `clozetext` `template` meant to be blocky and not inline $div = new Div(); $div->setClass('lrn-template'); $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($template))); // Build validation $isCaseSensitive = is_null($question->get_case_sensitive()) ? true : $question->get_case_sensitive(); $validationBuilder = new ClozetextValidationBuilder($isCaseSensitive); list($responseDeclaration, $responseProcessing) = $validationBuilder->buildValidation($interactionIdentifier, $question->get_validation(), $isCaseSensitive); return [$div, $responseDeclaration, $responseProcessing]; }
private function getFeatureReplacementString($node) { // Process inline feature if (isset($node->attr['data-type']) && isset($node->attr['data-src'])) { $src = trim($node->attr['data-src']); $type = trim($node->attr['data-type']); if ($type === 'audioplayer' || $type === 'audioplayer') { return QtiMarshallerUtil::marshallValidQti(new Object($src, MimeUtil::guessMimeType(basename($src)))); } // Process regular question feature } else { $nodeClassAttribute = $node->attr['class']; $featureReference = $this->getFeatureReferenceFromClassName($nodeClassAttribute); $feature = $this->widgets[$featureReference]; $type = $feature['data']['type']; if ($type === 'audioplayer' || $type === 'audioplayer') { $src = $feature['data']['src']; $object = new Object($src, MimeUtil::guessMimeType(basename($src))); $object->setLabel($featureReference); return QtiMarshallerUtil::marshallValidQti($object); } else { if ($type === 'sharedpassage') { $content = $feature['data']['content']; $object = new Object('', 'text/html'); $object->setContent(ContentCollectionBuilder::buildObjectFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($content))); $object->setLabel($featureReference); return QtiMarshallerUtil::marshallValidQti($object); } } } throw new MappingException($type . ' not supported'); }
public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel) { //TODO: Need validation a question shall have at least 1 {{response}} and 1 item in `possible_responses` /** @var clozeassociation $question */ $question = $questionType; // Replace {{ response }} with `gap` elements $index = 0; $template = preg_replace_callback('/{{response}}/', function ($match) use(&$index) { $gapIdentifier = self::GAP_IDENTIFIER_PREFIX . $index; $replacement = '<gap identifier="' . $gapIdentifier . '"/>'; $index++; return $replacement; }, $question->get_template()); $content = ContentCollectionBuilder::buildBlockStaticCollectionContent(QtiMarshallerUtil::unmarshallElement($template)); // Map `possible_responses` to gaps // TODO: Detect `img` $gapChoices = new GapChoiceCollection(); $possibleResponses = $question->get_possible_responses(); $matchMax = $question->get_duplicate_responses() ? count($possibleResponses) : 1; foreach ($possibleResponses as $index => $possibleResponse) { $gapChoice = new GapText(self::GAPCHOICE_IDENTIFIER_PREFIX . $index, $matchMax); $gapChoiceContent = new TextOrVariableCollection(); $gapChoiceContent->attach(new TextRun($possibleResponse)); $gapChoice->setContent($gapChoiceContent); $gapChoices->attach($gapChoice); } $interaction = new GapMatchInteraction($interactionIdentifier, $gapChoices, $content); $interaction->setLabel($interactionLabel); $interaction->setPrompt($this->convertStimulusForPrompt($question->get_stimulus())); $validationBuilder = new ClozeassociationValidationBuilder($possibleResponses); list($responseDeclaration, $responseProcessing) = $validationBuilder->buildValidation($interaction->getResponseIdentifier(), $question->get_validation()); return [$interaction, $responseDeclaration, $responseProcessing]; }
protected function convertStimulusForPrompt($stimulusString) { $stimulusComponents = QtiMarshallerUtil::unmarshallElement($stimulusString); $prompt = new Prompt(); $prompt->setContent(ContentCollectionBuilder::buildFlowStaticCollectionContent($stimulusComponents)); return $prompt; }
private function buildOptionCollection(choicematrix $question, $stemCount) { $optionIndexIdentifierMap = []; $optionCollection = new SimpleAssociableChoiceCollection(); foreach ($question->get_options() as $key => $optionValue) { // Learnosity's `choicematrix` always have its options to have any number of associable choice, thus setting to stems count // Same as above, won't validate upon empty response, thus setting match min to 1 $optionChoice = new SimpleAssociableChoice('OPTION_' . $key, $stemCount); $optionChoice->setMatchMin(1); $optionChoice->setContent(ContentCollectionBuilder::buildFlowStaticCollectionContent(QtiMarshallerUtil::unmarshallElement($optionValue))); $optionCollection->attach($optionChoice); $optionIndexIdentifierMap[$key] = $optionChoice->getIdentifier(); } return [$optionCollection, $optionIndexIdentifierMap]; }
protected function marshallChildrenKnown(QtiComponent $component, array $elements) { /** @var Object $component */ switch ($this->getMIMEType($component->getType())) { case self::MIME_IMAGE: $this->checkObjectComponents($component, '<img> tag'); $element = self::getDOMCradle()->createElement('img'); $element->setAttribute('src', $component->getData()); return $element; break; case self::MIME_AUDIO: $this->checkObjectComponents($component, '`audioplayer` feature'); $element = self::getDOMCradle()->createElement('span'); $element->setAttribute('class', 'learnosity-feature'); $element->setAttribute('data-type', 'audioplayer'); $element->setAttribute('data-src', $component->getData()); return $element; break; case self::MIME_VIDEO: $this->checkObjectComponents($component, '`videoplayer` feature'); $element = self::getDOMCradle()->createElement('span'); $element->setAttribute('class', 'learnosity-feature'); $element->setAttribute('data-type', 'videoplayer'); $element->setAttribute('data-src', $component->getData()); return $element; break; case self::MIME_HTML: $fragment = self::getDOMCradle()->createDocumentFragment(); $fragment->appendXML(QtiMarshallerUtil::marshallCollection(ContentCollectionBuilder::buildFlowCollectionContent($component->getComponents()))); $element = self::getDOMCradle()->createElement('div'); $element->setAttribute('data-type', 'sharedpassage'); $element->appendChild($fragment); return $element; break; default: // TODO: Need to think external HTML object file, what we are going to do with them? // Just parse <object> as default LogService::log('Unknown <object> MIME type, outputting <object> as it is'); return parent::marshallChildrenKnown($component, $elements); } }
public function convert(BaseQuestionType $questionType, $interactionIdentifier, $interactionLabel) { /** @var clozedropdown $question */ $question = $questionType; // Extra text that can't be mapped since we are in textEntryInteraction which does not have prompt $this->extraContent = $question->get_stimulus(); // Replace {{ response }} with `textEntryInteraction` elements $valueIdentifierMapPerInlineChoices = []; $index = 0; $possibleResponses = $question->get_possible_responses(); $template = preg_replace_callback('/{{response}}/', function ($match) use(&$index, &$valueIdentifierMapPerInlineChoices, $possibleResponses, $interactionIdentifier, $interactionLabel) { $inlineChoiceCollection = new InlineChoiceCollection(); if (!isset($possibleResponses[$index])) { throw new MappingException('Invalid `possible_responses`, missing entries'); } foreach ($possibleResponses[$index] as $choiceIndex => $choiceValue) { $inlineChoiceIdentifier = 'INLINECHOICE_' . $choiceIndex; $valueIdentifierMapPerInlineChoices[$index][$choiceValue] = $inlineChoiceIdentifier; // Update this map so can be used later upon building responseDeclaration objects $inlineChoice = new InlineChoice($inlineChoiceIdentifier); $inlineChoiceContent = new TextOrVariableCollection(); $inlineChoiceContent->attach(new TextRun($choiceValue)); $inlineChoice->setContent($inlineChoiceContent); $inlineChoiceCollection->attach($inlineChoice); } $interaction = new InlineChoiceInteraction($interactionIdentifier . '_' . $index, $inlineChoiceCollection); $interaction->setLabel($interactionLabel); $index++; $replacement = QtiMarshallerUtil::marshall($interaction); return $replacement; }, $question->get_template()); // Wrap this interaction in a block since our `clozedropdown` `template` meant to be blocky and not inline $div = new Div(); $div->setClass('lrn-template'); $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent(QtiMarshallerUtil::unmarshallElement($template))); // Build validation $validationBuilder = new ClozedropdownValidationBuilder($valueIdentifierMapPerInlineChoices); list($responseDeclaration, $responseProcessing) = $validationBuilder->buildValidation($interactionIdentifier, $question->get_validation()); return [$div, $responseDeclaration, $responseProcessing]; }
private function buildItemBodySimple(array $interactions) { $interactions = array_values($interactions); $contentCollection = new QtiComponentCollection(); // Append the extra contents belong to an interaction before the interaction itself foreach ($interactions as $data) { if (isset($data['extraContent'])) { $content = QtiMarshallerUtil::unmarshallElement($data['extraContent']); $contentCollection->merge($content); } $contentCollection->attach($data['interaction']); } $itemBody = new ItemBody(); $itemBody->setContent(ContentCollectionBuilder::buildBlockCollectionContent($contentCollection)); return $itemBody; }