libdir/xmlize.php"); class qformat_blackboard_6 extends qformat_default { function provide_import() { return true; } //Function to check and create the needed dir to unzip file to function check_and_create_import_dir($unique_code) { global $CFG; $status = $this->check_dir_exists($CFG->dataroot."/temp",true); if ($status) { $status = $this->check_dir_exists($CFG->dataroot."/temp/bbquiz_import",true); } if ($status) { $status = $this->check_dir_exists($CFG->dataroot."/temp/bbquiz_import/".$unique_code,true); } return $status; } function clean_temp_dir($dir='') { // for now we will just say everything happened okay note // that a mess may be piling up in $CFG->dataroot/temp/bbquiz_import // TODO return true at top of the function renders all the following code useless return true; if ($dir == '') { $dir = $this->temp_dir; } $slash = "/"; // Create arrays to store files and directories $dir_files = array(); $dir_subdirs = array(); // Make sure we can delete it chmod($dir, 0777); if ((($handle = opendir($dir))) == FALSE) { // The directory could not be opened return false; } // Loop through all directory entries, and construct two temporary arrays containing files and sub directories while(false !== ($entry = readdir($handle))) { if (is_dir($dir. $slash .$entry) && $entry != ".." && $entry != ".") { $dir_subdirs[] = $dir. $slash .$entry; } else if ($entry != ".." && $entry != ".") { $dir_files[] = $dir. $slash .$entry; } } // Delete all files in the curent directory return false and halt if a file cannot be removed for($i=0; $iclean_temp_dir($dir_subdirs[$i]) == FALSE) { return false; } else { if (rmdir($dir_subdirs[$i]) == FALSE) { return false; } } } // Close directory closedir($handle); if (rmdir($this->temp_dir) == FALSE) { return false; } // Success, every thing is gone return true return true; } //Function to check if a directory exists and, optionally, create it function check_dir_exists($dir,$create=false) { global $CFG; $status = true; if(!is_dir($dir)) { if (!$create) { $status = false; } else { umask(0000); $status = mkdir ($dir,$CFG->directorypermissions); } } return $status; } function importpostprocess() { /// Does any post-processing that may be desired /// Argument is a simple array of question ids that /// have just been added. // need to clean up temporary directory return $this->clean_temp_dir(); } function copy_file_to_course($filename) { global $CFG, $COURSE; $filename = str_replace('\\','/',$filename); $fullpath = $this->temp_dir.'/res00001/'.$filename; $basename = basename($filename); $copy_to = $CFG->dataroot.'/'.$COURSE->id.'/bb_import'; if ($this->check_dir_exists($copy_to,true)) { if(is_readable($fullpath)) { $copy_to.= '/'.$basename; if (!copy($fullpath, $copy_to)) { return false; } else { return $copy_to; } } } else { return false; } } function readdata($filename) { /// Returns complete file with an array, one item per line global $CFG; // if the extension is .dat we just return that, // if .zip we unzip the file and get the data $ext = substr($this->realfilename, strpos($this->realfilename,'.'), strlen($this->realfilename)-1); if ($ext=='.dat') { if (!is_readable($filename)) { error("File is not readable"); } return file($filename); } $unique_code = time(); $temp_dir = $CFG->dataroot."/temp/bbquiz_import/".$unique_code; $this->temp_dir = $temp_dir; if ($this->check_and_create_import_dir($unique_code)) { if(is_readable($filename)) { if (!copy($filename, "$temp_dir/bboard.zip")) { error("Could not copy backup file"); } if(unzip_file("$temp_dir/bboard.zip", '', false)) { // assuming that the information is in res0001.dat // after looking at 6 examples this was always the case $q_file = "$temp_dir/res00001.dat"; if (is_file($q_file)) { if (is_readable($q_file)) { $filearray = file($q_file); /// Check for Macintosh OS line returns (ie file on one line), and fix if (ereg("\r", $filearray[0]) AND !ereg("\n", $filearray[0])) { return explode("\r", $filearray[0]); } else { return $filearray; } } } else { error("Could not find question data file in zip"); } } else { print "filename: $filename
tempdir: $temp_dir
"; error("Could not unzip file."); } } else { error ("Could not read uploaded file"); } } else { error("Could not create temporary directory"); } } function save_question_options($question) { return true; } function readquestions ($lines) { /// Parses an array of lines into an array of questions, /// where each item is a question object as defined by /// readquestion(). $text = implode($lines, " "); $xml = xmlize($text, 0); $raw_questions = $xml['questestinterop']['#']['assessment'][0]['#']['section'][0]['#']['item']; $questions = array(); foreach($raw_questions as $quest) { $question = $this->create_raw_question($quest); switch($question->qtype) { case "Matching": $this->process_matching($question, $questions); break; case "Multiple Choice": $this->process_mc($question, $questions); break; case "Essay": $this->process_essay($question, $questions); break; case "Multiple Answer": $this->process_ma($question, $questions); break; case "True/False": $this->process_tf($question, $questions); break; case 'Fill in the Blank': $this->process_fblank($question, $questions); break; case 'Short Response': $this->process_essay($question, $questions); break; default: print "Unknown or unhandled question type: \"$question->qtype\"
"; break; } } return $questions; } // creates a cleaner object to deal with for processing into moodle // the object created is NOT a moodle question object function create_raw_question($quest) { $question = new StdClass; $question->qtype = $quest['#']['itemmetadata'][0]['#']['bbmd_questiontype'][0]['#']; $question->id = $quest['#']['itemmetadata'][0]['#']['bbmd_asi_object_id'][0]['#']; $presentation->blocks = $quest['#']['presentation'][0]['#']['flow'][0]['#']['flow']; foreach($presentation->blocks as $pblock) { $block = NULL; $block->type = $pblock['@']['class']; switch($block->type) { case 'QUESTION_BLOCK': $sub_blocks = $pblock['#']['flow']; foreach($sub_blocks as $sblock) { //echo "Calling process_block from line 263
"; $this->process_block($sblock, $block); } break; case 'RESPONSE_BLOCK': $choices = NULL; switch($question->qtype) { case 'Matching': $bb_subquestions = $pblock['#']['flow']; $sub_questions = array(); foreach($bb_subquestions as $bb_subquestion) { $sub_question = NULL; $sub_question->ident = $bb_subquestion['#']['response_lid'][0]['@']['ident']; $this->process_block($bb_subquestion['#']['flow'][0], $sub_question); $bb_choices = $bb_subquestion['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; $choices = array(); $this->process_choices($bb_choices, $choices); $sub_question->choices = $choices; if (!isset($block->subquestions)) { $block->subquestions = array(); } $block->subquestions[] = $sub_question; } break; case 'Multiple Answer': $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; $choices = array(); $this->process_choices($bb_choices, $choices); $block->choices = $choices; break; case 'Essay': // Doesn't apply since the user responds with text input break; case 'Multiple Choice': $mc_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label']; foreach($mc_choices as $mc_choice) { $choices = NULL; $choices = $this->process_block($mc_choice, $choices); $block->choices[] = $choices; } break; case 'Short Response': // do nothing? break; case 'Fill in the Blank': // do nothing? break; case 'Fill in the Blank Plus': // do nothing? break; case 'Numeric': // do nothing? break; default: $bb_choices = $pblock['#']['response_lid'][0]['#']['render_choice'][0]['#']['flow_label'][0]['#']['response_label']; $choices = array(); $this->process_choices($bb_choices, $choices); $block->choices = $choices; } break; case 'RIGHT_MATCH_BLOCK': $matching_answerset = $pblock['#']['flow']; $answerset = array(); foreach($matching_answerset as $answer) { // $answerset[] = $this->process_block($answer, $bb_answer); $bb_answer = null; $bb_answer->text = $answer['#']['flow'][0]['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; $answerset[] = $bb_answer; } $block->matching_answerset = $answerset; break; default: print "UNHANDLED PRESENTATION BLOCK"; break; } $question->{$block->type} = $block; } // determine response processing // there is a section called 'outcomes' that I don't know what to do with $resprocessing = $quest['#']['resprocessing']; $respconditions = $resprocessing[0]['#']['respcondition']; $responses = array(); if ($question->qtype == 'Matching') { $this->process_matching_responses($respconditions, $responses); } else { $this->process_responses($respconditions, $responses); } $question->responses = $responses; $feedbackset = $quest['#']['itemfeedback']; $feedbacks = array(); $this->process_feedback($feedbackset, $feedbacks); $question->feedback = $feedbacks; return $question; } function process_block($cur_block, &$block) { global $COURSE, $CFG; $cur_type = $cur_block['@']['class']; switch($cur_type) { case 'FORMATTED_TEXT_BLOCK': $block->text = $this->strip_applet_tags_get_mathml($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']); break; case 'FILE_BLOCK': //revisit this to make sure it is working correctly // Commented out ['matapplication']..., etc. because I // noticed that when I imported a new Blackboard 6 file // and printed out the block, the tree did not extend past ['material'][0]['#'] - CT 8/3/06 $block->file = $cur_block['#']['material'][0]['#'];//['matapplication'][0]['@']['uri']; if ($block->file != '') { // if we have a file copy it to the course dir and adjust its name to be visible over the web. $block->file = $this->copy_file_to_course($block->file); $block->file = $CFG->wwwroot.'/file.php/'.$COURSE->id.'/bb_import/'.basename($block->file); } break; case 'Block': if (isset($cur_block['#']['material'][0]['#']['mattext'][0]['#'])) { $block->text = $cur_block['#']['material'][0]['#']['mattext'][0]['#']; } else if (isset($cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#'])) { $block->text = $cur_block['#']['material'][0]['#']['mat_extension'][0]['#']['mat_formattedtext'][0]['#']; } else if (isset($cur_block['#']['response_label'])) { // this is a response label block $sub_blocks = $cur_block['#']['response_label'][0]; if(!isset($block->ident)) { if(isset($sub_blocks['@']['ident'])) { $block->ident = $sub_blocks['@']['ident']; } } foreach($sub_blocks['#']['flow_mat'] as $sub_block) { $this->process_block($sub_block, $block); } } else { if (isset($cur_block['#']['flow_mat']) || isset($cur_block['#']['flow'])) { if (isset($cur_block['#']['flow_mat'])) { $sub_blocks = $cur_block['#']['flow_mat']; } elseif (isset($cur_block['#']['flow'])) { $sub_blocks = $cur_block['#']['flow']; } foreach ($sub_blocks as $sblock) { // this will recursively grab the sub blocks which should be of one of the other types $this->process_block($sblock, $block); } } } break; case 'LINK_BLOCK': // not sure how this should be included if (!empty($cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri'])) { $block->link = $cur_block['#']['material'][0]['#']['mattext'][0]['@']['uri']; } else { $block->link = ''; } break; } return $block; } function process_choices($bb_choices, &$choices) { foreach($bb_choices as $choice) { if (isset($choice['@']['ident'])) { $cur_choice = $choice['@']['ident']; } else { //for multiple answer $cur_choice = $choice['#']['response_label'][0];//['@']['ident']; } if (isset($choice['#']['flow_mat'][0])) { //for multiple answer $cur_block = $choice['#']['flow_mat'][0]; // Reset $cur_choice to NULL because process_block is expecting an object // for the second argument and not a string, which is what is was set as // originally - CT 8/7/06 $cur_choice = null; $this->process_block($cur_block, $cur_choice); } elseif (isset($choice['#']['response_label'])) { // Reset $cur_choice to NULL because process_block is expecting an object // for the second argument and not a string, which is what is was set as // originally - CT 8/7/06 $cur_choice = null; $this->process_block($choice, $cur_choice); } $choices[] = $cur_choice; } } function process_matching_responses($bb_responses, &$responses) { foreach($bb_responses as $bb_response) { $response = NULL; if (isset($bb_response['#']['conditionvar'][0]['#']['varequal'])) { $response->correct = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['#']; $response->ident = $bb_response['#']['conditionvar'][0]['#']['varequal'][0]['@']['respident']; } else { $response->correct = 'Broken Question?'; $response->ident = 'Broken Question?'; } $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; $responses[] = $response; } } function process_responses($bb_responses, &$responses) { foreach($bb_responses as $bb_response) { //Added this line to instantiate $response. // Without instantiating the $response variable, the same object // gets added to the array $response = null; if (isset($bb_response['@']['title'])) { $response->title = $bb_response['@']['title']; } else { $reponse->title = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; } $reponse->ident = array(); if (isset($bb_response['#']['conditionvar'][0]['#'])){//['varequal'][0]['#'])) { $response->ident[0] = $bb_response['#']['conditionvar'][0]['#'];//['varequal'][0]['#']; } else if (isset($bb_response['#']['conditionvar'][0]['#']['other'][0]['#'])) { $response->ident[0] = $bb_response['#']['conditionvar'][0]['#']['other'][0]['#']; } if (isset($bb_response['#']['conditionvar'][0]['#']['and'])){//[0]['#'])) { $responseset = $bb_response['#']['conditionvar'][0]['#']['and'];//[0]['#']['varequal']; foreach($responseset as $rs) { $response->ident[] = $rs['#']; if(!isset($response->feedback) and isset( $rs['@'] ) ) { $response->feedback = $rs['@']['respident']; } } } else { $response->feedback = $bb_response['#']['displayfeedback'][0]['@']['linkrefid']; } // determine what point value to give response if (isset($bb_response['#']['setvar'])) { switch ($bb_response['#']['setvar'][0]['#']) { case "SCORE.max": $response->fraction = 1; break; default: // I have only seen this being 0 or unset // there are probably fractional values of SCORE.max, but I'm not sure what they look like $response->fraction = 0; break; } } else { // just going to assume this is the case this is probably not correct. $response->fraction = 0; } $responses[] = $response; } } function process_feedback($feedbackset, &$feedbacks) { foreach($feedbackset as $bb_feedback) { // Added line $feedback=null so that $feedback does not get reused in the loop // and added the the $feedbacks[] array multiple times $feedback = null; $feedback->ident = $bb_feedback['@']['ident']; if (isset($bb_feedback['#']['flow_mat'][0])) { $this->process_block($bb_feedback['#']['flow_mat'][0], $feedback); } elseif (isset($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0])) { $this->process_block($bb_feedback['#']['solution'][0]['#']['solutionmaterial'][0]['#']['flow_mat'][0], $feedback); } $feedbacks[] = $feedback; } } /** * Create common parts of question */ function process_common( $quest ) { $question = $this->defaultquestion(); $question->questiontext = addslashes($quest->QUESTION_BLOCK->text); $question->name = shorten_text( $quest->id, 250 ); return $question; } //---------------------------------------- // Process True / False Questions //---------------------------------------- function process_tf($quest, &$questions) { $question = $this->process_common( $quest ); $question->qtype = TRUEFALSE; $question->single = 1; // Only one answer is allowed // 0th [response] is the correct answer. $responses = $quest->responses; $correctresponse = $responses[0]->ident[0]['varequal'][0]['#']; if ($correctresponse != 'false') { $correct = true; } else { $correct = false; } foreach($quest->feedback as $fb) { $fback->{$fb->ident} = $fb->text; } if ($correct) { // true is correct $question->answer = 1; $question->feedbacktrue = addslashes($fback->correct); $question->feedbackfalse = addslashes($fback->incorrect); } else { // false is correct $question->answer = 0; $question->feedbacktrue = addslashes($fback->incorrect); $question->feedbackfalse = addslashes($fback->correct); } $question->correctanswer = $question->answer; $questions[] = $question; } //---------------------------------------- // Process Fill in the Blank //---------------------------------------- function process_fblank($quest, &$questions) { $question = $this->process_common( $quest ); $question->qtype = SHORTANSWER; $question->single = 1; $answers = array(); $fractions = array(); $feedbacks = array(); // extract the feedback $feedback = array(); foreach($quest->feedback as $fback) { if (isset($fback->ident)) { if ($fback->ident == 'correct' || $fback->ident == 'incorrect') { $feedback[$fback->ident] = $fback->text; } } } foreach($quest->responses as $response) { if(isset($response->title)) { if (isset($response->ident[0]['varequal'][0]['#'])) { //for BB Fill in the Blank, only interested in correct answers if ($response->feedback = 'correct') { $answers[] = addslashes($response->ident[0]['varequal'][0]['#']); $fractions[] = 1; if (isset($feedback['correct'])) { $feedbacks[] = addslashes($feedback['correct']); } else { $feedbacks[] = ''; } } } } } //Adding catchall to so that students can see feedback for incorrect answers when they enter something the //instructor did not enter $answers[] = '*'; $fractions[] = 0; if (isset($feedback['incorrect'])) { $feedbacks[] = addslashes($feedback['incorrect']); } else { $feedbacks[] = ''; } $question->answer = $answers; $question->fraction = $fractions; $question->feedback = $feedbacks; // Changed to assign $feedbacks to $question->feedback instead of if (!empty($question)) { $questions[] = $question; } } //---------------------------------------- // Process Multiple Choice Questions //---------------------------------------- function process_mc($quest, &$questions) { $question = $this->process_common( $quest ); $question->qtype = MULTICHOICE; $question->single = 1; $feedback = array(); foreach($quest->feedback as $fback) { $feedback[$fback->ident] = addslashes($fback->text); } foreach($quest->responses as $response) { if (isset($response->title)) { if ($response->title == 'correct') { // only one answer possible for this qtype so first index is correct answer $correct = $response->ident[0]['varequal'][0]['#']; } } else { // fallback method for when the title is not set if ($response->feedback == 'correct') { // only one answer possible for this qtype so first index is correct answer $correct = $response->ident[0]['varequal'][0]['#']; // added [0]['varequal'][0]['#'] to $response->ident - CT 8/9/06 } } } $i = 0; foreach($quest->RESPONSE_BLOCK->choices as $response) { $question->answer[$i] = addslashes($response->text); if ($correct == $response->ident) { $question->fraction[$i] = 1; // this is a bit of a hack to catch the feedback... first we see if a 'correct' feedback exists // then specific feedback for this question (maybe this should be switched?, but from my example // question pools I have not seen response specific feedback, only correct or incorrect feedback if (!empty($feedback['correct'])) { $question->feedback[$i] = $feedback['correct']; } elseif (!empty($feedback[$i])) { $question->feedback[$i] = $feedback[$i]; } else { // failsafe feedback (should be '' instead?) $question->feedback[$i] = "correct"; } } else { $question->fraction[$i] = 0; if (!empty($feedback['incorrect'])) { $question->feedback[$i] = $feedback['incorrect']; } elseif (!empty($feedback[$i])) { $question->feedback[$i] = $feedback[$i]; } else { // failsafe feedback (should be '' instead?) $question->feedback[$i] = 'incorrect'; } } $i++; } if (!empty($question)) { $questions[] = $question; } } //---------------------------------------- // Process Multiple Choice Questions With Multiple Answers //---------------------------------------- function process_ma($quest, &$questions) { $question = $this->process_common( $quest ); // copied this from process_mc $question->qtype = MULTICHOICE; $question->single = 0; // More than one answer allowed $answers = $quest->responses; $correct_answers = array(); foreach($answers as $answer) { if($answer->title == 'correct') { $answerset = $answer->ident[0]['and'][0]['#']['varequal']; foreach($answerset as $ans) { $correct_answers[] = $ans['#']; } } } foreach ($quest->feedback as $fb) { $feedback->{$fb->ident} = addslashes(trim($fb->text)); } $correct_answer_count = count($correct_answers); $choiceset = $quest->RESPONSE_BLOCK->choices; $i = 0; foreach($choiceset as $choice) { $question->answer[$i] = addslashes(trim($choice->text)); if (in_array($choice->ident, $correct_answers)) { // correct answer $question->fraction[$i] = floor(100000/$correct_answer_count)/100000; // strange behavior if we have more than 5 decimal places $question->feedback[$i] = $feedback->correct; } else { // wrong answer $question->fraction[$i] = 0; $question->feedback[$i] = $feedback->incorrect; } $i++; } $questions[] = $question; } //---------------------------------------- // Process Essay Questions //---------------------------------------- function process_essay($quest, &$questions) { // this should be rewritten to accomodate moodle 1.6 essay question type eventually if (defined("ESSAY")) { // treat as short answer $question = $this->process_common( $quest ); // copied this from process_mc $question->qtype = ESSAY; $question->feedback = array(); // not sure where to get the correct answer from foreach($quest->feedback as $feedback) { // Added this code to put the possible solution that the // instructor gives as the Moodle answer for an essay question if ($feedback->ident == 'solution') { $question->feedback = addslashes($feedback->text); } } //Added because essay/questiontype.php:save_question_option is expecting a //fraction property - CT 8/10/06 $question->fraction[] = 1; if (!empty($question)) { $questions[]=$question; } } else { print "Essay question types are not handled because the quiz question type 'Essay' does not exist in this installation of Moodle
"; print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

'; } } //---------------------------------------- // Process Matching Questions //---------------------------------------- function process_matching($quest, &$questions) { global $QTYPES; // renderedmatch is an optional plugin, so we need to check if it is defined if (array_key_exists('renderedmatch', $QTYPES)) { $question = $this->process_common( $quest ); $question->valid = true; $question->qtype = 'renderedmatch'; foreach($quest->RESPONSE_BLOCK->subquestions as $qid => $subq) { foreach($quest->responses as $rid => $resp) { if ($resp->ident == $subq->ident) { $correct = addslashes($resp->correct); $feedback = addslashes($resp->feedback); } } foreach($subq->choices as $cid => $choice) { if ($choice == $correct) { $question->subquestions[] = addslashes($subq->text); $question->subanswers[] = addslashes($quest->RIGHT_MATCH_BLOCK->matching_answerset[$cid]->text); } } } // check format $status = true; if ( count($quest->RESPONSE_BLOCK->subquestions) > count($quest->RIGHT_MATCH_BLOCK->matching_answerset) || count($question->subquestions) < 2) { $status = false; } else { // need to redo to make sure that no two questions have the same answer (rudimentary now) foreach($question->subanswers as $qstn) { if(isset($previous)) { if ($qstn == $previous) { $status = false; } } $previous = $qstn; if ($qstn == '') { $status = false; } } } if ($status) { $questions[] = $question; } else { global $COURSE, $CFG; print ''; print ''; print ""; print "'; print '
This matching question is malformed. Please ensure there are no blank answers, no two questions have the same answer, and/or there are correct answers for each question. There must be at least as many subanswers as subquestions, and at least one subquestion.
Question:".$quest->QUESTION_BLOCK->text; if (isset($quest->QUESTION_BLOCK->file)) { print '
There is a subfile contained in the zipfile that has been copied to course files: bb_import/'.basename($quest->QUESTION_BLOCK->file).''; if (preg_match('/(gif|jpg|jpeg|png)$/i', $quest->QUESTION_BLOCK->file)) { print ''; } } print "
Subquestions:
    "; foreach($quest->responses as $rs) { $correct_responses->{$rs->ident} = $rs->correct; } foreach($quest->RESPONSE_BLOCK->subquestions as $subq) { print '
  • '.$subq->text.'
      '; foreach($subq->choices as $id=>$choice) { print '
    • '; if ($choice == $correct_responses->{$subq->ident}) { print ''; } else { print ''; } print $quest->RIGHT_MATCH_BLOCK->matching_answerset[$id]->text.'
    • '; } print '
    '; } print '
Feedback:
    '; foreach($quest->feedback as $fb) { print '
  • '.$fb->ident.': '.$fb->text.'
  • '; } print '
'; } } else { print "Matching question types are not handled because the quiz question type 'Rendered Matching' does not exist in this installation of Moodle
"; print "    Omitted Question: ".$quest->QUESTION_BLOCK->text.'

'; } } function strip_applet_tags_get_mathml($string) { if(stristr($string, '') === FALSE) { return $string; } else { // strip all applet tags keeping stuff before/after and inbetween (if mathml) them while (stristr($string, '') !== FALSE) { preg_match("/(.*)\.*\<\/math\>)\".*\<\/applet\>(.*)/i",$string, $mathmls); $string = $mathmls[1].$mathmls[2].$mathmls[3]; } return $string; } } } // close object ?>