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;
 }