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