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];
 }
 private function buildGapImgCollection(array $possibleResponses, $matchMax)
 {
     $gapImageCollection = new GapImgCollection();
     foreach ($possibleResponses as $index => $possibleResponse) {
         $html = new SimpleHtmlDom();
         $html->load($possibleResponse);
         $img = $html->find('img');
         // Detect `img` and make sure it is an image
         if (count($img) === 1) {
             // TODO: Validation these attributes exists
             $src = $img[0]->src;
             $imagesize = getimagesize(CurlUtil::prepareUrlForCurl($src));
             $gapImageObject = new Object($src, $imagesize['mime']);
             $gapImageObject->setWidth($imagesize[0]);
             $gapImageObject->setHeight($imagesize[1]);
             // No `img` assuming its all text
         } elseif (count($img) === 0) {
             $gapImageObject = $this->convertTextToObjectWithBase64ImageString($possibleResponse);
         } else {
             throw new MappingException('Does not support mapping `possible_responses` as HTML, has to be either just a single `image` or `text`');
         }
         $gapImageCollection->attach(new GapImg(self::GAPIMG_IDENTIFIER_PREFIX . $index, $matchMax, $gapImageObject));
     }
     return $gapImageCollection;
 }
 public function buildItemBody(array $interactions, $content = '')
 {
     // Try to build the <itemBody> according to items` content if exists
     if (empty($content)) {
         return $this->buildItemBodySimple($interactions);
     }
     try {
         return $this->buildItemBodyWithItemContent($interactions, $content);
         // If anything fails, <itemBody> can't be mapped due to whatever reasons
         // Probably simply due to its being wrapped in a tag which only accept inline content
         // Simply build it without considering items` content and put the content on the top
     } catch (\Exception $e) {
         $itemBody = $this->buildItemBodySimple($interactions);
         $itemBodyContent = new BlockCollection();
         // Build the div bundle that contains all the item`s content
         // minus those questions and features `span`
         $html = new SimpleHtmlDom();
         $html->load($content);
         foreach ($html->find('span.learnosity-response') as &$span) {
             $span->outertext = '';
         }
         $div = new Div();
         $contentCollection = QtiMarshallerUtil::unmarshallElement($html->save());
         $div->setContent(ContentCollectionBuilder::buildFlowCollectionContent($contentCollection));
         $itemBodyContent->attach($div);
         $itemBodyContent->merge($itemBody->getComponents());
         $itemBody->setContent($itemBodyContent);
         LogService::log('Interactions are failed to be mapped with `item` content: ' . $e->getMessage() . '. Thus, interactions are separated from its actual `item` content and appended in the bottom');
         return $itemBody;
     }
 }
 public function map($itemReference, ItemBody $itemBody, QtiComponentCollection $interactionComponents, QtiComponentCollection $responseDeclarations = null, ResponseProcessingTemplate $responseProcessingTemplate = null)
 {
     $this->itemReference = $itemReference;
     $questionsXmls = [];
     $responseDeclarationsMap = [];
     if ($responseDeclarations) {
         /** @var ResponseDeclaration $responseDeclaration */
         foreach ($responseDeclarations as $responseDeclaration) {
             $responseDeclarationsMap[$responseDeclaration->getIdentifier()] = $responseDeclaration;
         }
     }
     foreach ($interactionComponents as $component) {
         /* @var $component Interaction */
         $questionReference = $this->itemReference . '_' . $component->getResponseIdentifier();
         // Process <responseDeclaration>
         $responseDeclaration = isset($responseDeclarationsMap[$component->getResponseIdentifier()]) ? $responseDeclarationsMap[$component->getResponseIdentifier()] : null;
         $mapper = $this->getMapperInstance($component->getQtiClassName(), [$component, $responseDeclaration, $responseProcessingTemplate]);
         $question = $mapper->getQuestionType();
         $this->questions[$questionReference] = new Question($question->get_type(), $questionReference, $question);
         $questionsXmls[$questionReference] = ['qtiClassName' => $component->getQtiClassName(), 'responseIdentifier' => $component->getResponseIdentifier()];
     }
     // Build item's HTML content
     $extraContentHtml = new SimpleHtmlDom();
     if (!$extraContentHtml->load(QtiMarshallerUtil::marshallCollection($itemBody->getComponents()), false)) {
         throw new \Exception('Issues with the content for itemBody, it might not be valid');
     }
     foreach ($questionsXmls as $questionReference => $interactionData) {
         // Append this question span to our `item` content as it is
         $this->content .= '<span class="learnosity-response question-' . $questionReference . '"></span>';
         // Clean up interaction HTML content
         $qtiClassName = $interactionData['qtiClassName'];
         $responseIdentifier = $interactionData['responseIdentifier'];
         $toFind = $qtiClassName . '[responseIdentifier="' . $responseIdentifier . '"]';
         foreach ($extraContentHtml->find($toFind) as &$tag) {
             $tag->outertext = '';
         }
     }
     $extraContent = $extraContentHtml->save();
     // Making assumption question always has stimulus `right`?
     // So, prepend the extra content on the stimulus on the first question
     if (!empty(trim($extraContent))) {
         $firstQuestionReference = key($this->questions);
         $newStimulus = $extraContent . $this->questions[$firstQuestionReference]->get_data()->get_stimulus();
         $this->questions[$firstQuestionReference]->get_data()->set_stimulus($newStimulus);
         LogService::log('Extra <itemBody> content is prepended to question stimulus and please verify as this `might` break item content structure');
     }
     return true;
 }
 private function processHtml($content)
 {
     $html = new SimpleHtmlDom();
     $html->load($content);
     // Replace <br> with <br />, <img ....> with <img />, etc
     /** @var array $selfClosingTags ie. `img, br, input, meta, link, hr, base, embed, spacer` */
     $selfClosingTags = implode(array_keys($html->getSelfClosingTags()), ', ');
     foreach ($html->find($selfClosingTags) as &$node) {
         $node->outertext = rtrim($node->outertext, '>') . '/>';
     }
     // Replace these audioplayer and videoplayer feature with <object> nodes
     foreach ($html->find('span.learnosity-feature') as &$node) {
         try {
             // Replace <span..> with <object..>
             $replacement = $this->getFeatureReplacementString($node);
             $node->outertext = $replacement;
         } catch (MappingException $e) {
             LogService::log($e->getMessage() . '. Ignoring mapping feature ' . $node->outertext . '`');
         }
     }
     return $html->save();
 }