#include #include #include #include /* for malloc(),etc */ #include #include #include #include "../global.h" #include "../xmlparse.h" /* header for libexpat.a */ #include "shared_util.h" /* for cs_critical_error() */ #include "shared_cs_util.h" /* for cs_critical_error() */ #include "shared_hotpot_parser.h" /* data types for this module */ QUIZ_DATA quiz_data; char stored_text[MAX_TEXT]; char hotpot_answer_letters[MAX_HOTPOT_CHOICES + 1] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; /** Hot Potatoes 6 uses a hexadecimal escape sequence to represent *** < and >, while Hot Potatoes 5.x uses < and > *** *** This parser needs to 'unescape' these character sequences, *** which start with the following string of characters: **/ #define ESCAPE_CHARS "�" static void get_tag(const char *name, TAGS *tag) { if(!strcasecmp(name,"title")) *tag = TITLE; else if(!strcasecmp(name,"instructions")) *tag = INSTRUCTIONS; else if(!strcasecmp(name,"exercise-subtitle")) *tag = EXERCISE_SUBTITLE; else if(!strcasecmp(name,"case-sensitive")) *tag = CASE_SENSITIVE; else if(!strcasecmp(name,"question")) *tag = QUESTION; else if(!strcasecmp(name,"must-select-all")) *tag= MUST_SELECT_ALL; else if(!strcasecmp(name,"text")) *tag= TEXT; else if(!strcasecmp(name,"correct")) *tag= CORRECT; else if(!strcasecmp(name,"feedback")) *tag= FEEDBACK; else if(!strcasecmp(name,"page-bg-color")) *tag= PG_BG_COLOR; else if(!strcasecmp(name,"ex-bg-color")) *tag= EX_BG_COLOR; else if(!strcasecmp(name,"text-color")) *tag= TEXT_COLOR; else if(!strcasecmp(name,"title-color")) *tag= TITLE_COLOR; else if(!strcasecmp(name,"link-color")) *tag= LINK_COLOR; else if(!strcasecmp(name,"vlink-color")) *tag= VLINK_COLOR; else if(!strcasecmp(name,"font-face")) *tag= FONT_FACE; else if(!strcasecmp(name,"jquiz")) *tag= JQZ_QUIZ; else if(!strcasecmp(name,"hotpot-jbc-file")) *tag= JBC_QUIZ; else if(!strcasecmp(name, "version")) *tag = HOTPOT_VERSION; else if(!strcasecmp(name, "question-type")) *tag = QUES_TYPE; else if(!strcasecmp(name, "include-in-mc-options")) *tag = INCLUDE_IN_MC_OPTIONS; else *tag= UNUSED; } /** If string starts with a Hot Potatoes 6 *** escape sequence of the form: *** *** C; *** *** then this function: *** returns 1 *** sets unescaped_char to the character represented *** by the sequence *** returns in escape_len, the the length of the *** escape sequence. *** *** This is really necessary because Hot Potatoes *** version 6 writes: *** *** C; when it should write < *** E; when it should write > *** *** *** WARNING: this modifies 'string' *** **/ static int unescape(char *string, char *unescaped_char, int *escape_len) { char *start_escape; char *end_escape; /* if string doesn't start with ESCAPE_CHARS, ** return 0 */ start_escape = strstr(string, ESCAPE_CHARS); if(!start_escape || (start_escape != string)) return 0; /* string must also contain a semi-colon ** for it to be a valid escape sequence */ end_escape = strchr(string,';'); if(!end_escape) return 0; *(string + 1) = '0'; /* change "�" to &0x00" because strol() requires it */ *unescaped_char = (char)strtol(string + 1, &end_escape, 16); /* note we skip leading & */ /* yet another test */ if(*end_escape != ';') return 0; /* how many characters made up the escape sequence */ *escape_len = (int) (end_escape - start_escape + 1); return 1; /* string started with an escape sequence */ } static void store_string (char **dest) { int escape_len; int len; char unescaped_char; int dest_x = 0, source_x = 0; len = strlen(stored_text) + 1; *dest = (char *)malloc(len * sizeof(char)); if(!strstr(stored_text,ESCAPE_CHARS)) strncpy(*dest,stored_text,len); else { while(source_x < len) { if(unescape( stored_text + source_x, &unescaped_char, &escape_len)) { *(*dest + dest_x) = unescaped_char; dest_x++; source_x += escape_len; } else { *(*dest + dest_x) = stored_text[source_x]; dest_x++; source_x++; } } } } static void new_question() { QUESTION_NODE *ptr; if(!quiz_data.last_question) { quiz_data.head = (QUESTION_NODE *)malloc(sizeof(QUESTION_NODE)); ptr = quiz_data.head; } else { quiz_data.last_question->next = (QUESTION_NODE *)malloc(sizeof(QUESTION_NODE)); ptr = quiz_data.last_question->next; } store_string(&(ptr->question)); ptr->response = 0; /* no answer yet from student */ switch(quiz_data.type) { case JBC: ptr->question_type = MC; break; /* ver 5 jbc is mc, ** but might be changed to mc-multi later if ** must-select-all is set **/ case JQZ: ptr->question_type = SHORT; break; /* ver 5 jqz is always short answer */ default: ptr->question_type = UNKNOWN_QUESTION_TYPE; break; /* for ver 6, we have to read the **question-type later */ } ptr->correct_answer_string = (char *) 0; ptr->correct_answer_count = 0; ptr->answer_correct = 0; /* don't know if student answered this correctly */ ptr->answer_count = 0; ptr->head = (ANSWER_NODE *) 0; ptr->last_answer = (ANSWER_NODE *) 0; ptr->next = (QUESTION_NODE *) 0; quiz_data.last_question = ptr; } static void new_answer() { ANSWER_NODE *ptr; if(!quiz_data.last_question) return; /* should never happen */ if(!quiz_data.last_question->last_answer) { quiz_data.last_question->head = (ANSWER_NODE *)malloc(sizeof(ANSWER_NODE)); ptr = quiz_data.last_question->head; } else { quiz_data.last_question->last_answer->next = (ANSWER_NODE *)malloc(sizeof(ANSWER_NODE)); ptr = quiz_data.last_question->last_answer->next; } ptr->is_correct = 0; ptr->feedback = 0; ptr->next = (ANSWER_NODE *) 0; quiz_data.last_question->last_answer = ptr; } /** the must-select-all tag should only appear in *** HOTPOT 5 JBC tests. If this value is one, *** and this is a HOTPOT 5 JBC quiz then change *** the question type to MC-MULTI **/ static void must_select_all() { int value; value = atoi(stored_text); if((value == 1) && (quiz_data.last_question) && ((quiz_data.type == JBC) || (quiz_data.last_question->question_type == UNKNOWN_QUESTION_TYPE)) ) quiz_data.last_question->question_type = MC_MULTI; } /* the question-type tag is new to HotPot 6 the values are: ** 1 - straight multiple-choice ** 2 - short answer ** 3 - 'hybrid' - Manhattan switches this to straight multiple-choice ** 4 - multi-select (multiple-choice where user has to select all of the correct answers **/ static void question_type() { int value; value = atoi(stored_text); if(value && (quiz_data.last_question)) { switch(value) { case 1: quiz_data.last_question->question_type = MC; break; case 2: quiz_data.last_question->question_type = SHORT; break; case 3: quiz_data.last_question->question_type = MC; break; /* hotpot 6 hybrid becomes multiple-choice */ case 4: quiz_data.last_question->question_type = MC_MULTI; break; default: quiz_data.last_question->question_type = UNKNOWN_QUESTION_TYPE; break; } } } /* include_in_mc_options is new to hotpot 6 ** it should only be '0' for JQZ6 quizzes, and multiple ** choice questions. If it is found to be zero, clear out ** the answer field, so we can skip it when it is presented */ static void include_in_mc_options() { int value; value = atoi(stored_text); if(!value && (quiz_data.last_question) && (quiz_data.last_question->question_type == MC) && (quiz_data.last_question->last_answer)) if(quiz_data.last_question->last_answer->answer) *(quiz_data.last_question->last_answer->answer) = '\0'; } static void answer_text() { if(quiz_data.last_question && quiz_data.last_question->last_answer) store_string(&(quiz_data.last_question->last_answer->answer)); } static void correct() { if(quiz_data.last_question && quiz_data.last_question->last_answer) quiz_data.last_question->last_answer->is_correct = atoi(stored_text); } static void feedback() { if(quiz_data.last_question && quiz_data.last_question->last_answer) store_string(&(quiz_data.last_question->last_answer->feedback)); } static void clear_stored_text() { if(*stored_text) memset(stored_text, 0, MAX_TEXT); } void startElement(void *userData, const char *name, const char **atts) { if(!strcasecmp(name,"answer")) new_answer(); clear_stored_text(); } static void jbc_quiz() { QUESTION_NODE *qptr; quiz_data.type = JBC; for(qptr=quiz_data.head; qptr; qptr=qptr->next) if(qptr->question_type == UNKNOWN_QUESTION_TYPE) qptr->question_type = MC; } static void jqz_quiz() { QUESTION_NODE *qptr; quiz_data.type = (quiz_data.version && strchr(quiz_data.version,'6'))? JQZ6: JQZ; if(quiz_data.type == JQZ) { for(qptr=quiz_data.head; qptr; qptr=qptr->next) if(qptr->question_type == UNKNOWN_QUESTION_TYPE) qptr->question_type = SHORT; } } void endElement(void *userData, const char *name) { TAGS tag; get_tag(name,&tag); switch(tag) { case TITLE: store_string(&quiz_data.title); break; case INSTRUCTIONS: store_string(&(quiz_data.instructions)); break; case EXERCISE_SUBTITLE: store_string(&(quiz_data.sub_title)); break; case CASE_SENSITIVE: quiz_data.case_sensitive = atoi(stored_text); break; case QUESTION: new_question(); break; case MUST_SELECT_ALL: must_select_all(); break; case TEXT: answer_text(); break; case CORRECT: correct(); break; case FEEDBACK: feedback(); break; case PG_BG_COLOR: store_string(&(quiz_data.pg_bg_color)); break; case EX_BG_COLOR: store_string(&(quiz_data.ex_bg_color)); break; case TITLE_COLOR: store_string(&(quiz_data.title_color)); break; case TEXT_COLOR: store_string(&(quiz_data.text_color)); break; case LINK_COLOR: store_string(&(quiz_data.link_color)); break; case VLINK_COLOR: store_string(&(quiz_data.vlink_color)); break; case FONT_FACE: store_string(&(quiz_data.font_face)); break; case JBC_QUIZ: jbc_quiz(); break; case JQZ_QUIZ: jqz_quiz(); break; case HOTPOT_VERSION: store_string(&(quiz_data.version)); break; case QUES_TYPE: question_type(); break; case INCLUDE_IN_MC_OPTIONS: include_in_mc_options(); break; default: break; } clear_stored_text(); } void char_handler(void *userData, const char *txt, int len) { strncat(stored_text,txt,len); } static void init_quiz_data() { quiz_data.type = UNKNOWN_QUIZ; quiz_data.version = 0; quiz_data.title = 0; quiz_data.sub_title = 0; quiz_data.instructions = 0; quiz_data.case_sensitive = 0; quiz_data.pg_bg_color = 0; quiz_data.ex_bg_color = 0; quiz_data.title_color = 0; quiz_data.text_color = 0; quiz_data.link_color = 0; quiz_data.vlink_color = 0; quiz_data.font_face = 0; quiz_data.head = 0; quiz_data.last_question = 0; clear_stored_text(); } static void free_answer_node(ANSWER_NODE *aptr) { if(aptr->answer) { free(aptr->answer); aptr->answer = 0; } if(aptr->feedback) { free(aptr->feedback); aptr->feedback = 0; } free(aptr); } static void free_question_node(QUESTION_NODE *qptr) { ANSWER_NODE *aptr; if(qptr->question) { free(qptr->question); qptr->question = 0; } if(qptr->response) { free(qptr->response); qptr->response = 0; } if(qptr->correct_answer_string) { free(qptr->correct_answer_string); qptr->correct_answer_string = 0; } while(qptr->head) { aptr = qptr->head; qptr->head = qptr->head->next; free_answer_node(aptr); } free(qptr); } void free_quiz_data() { QUESTION_NODE *qptr; if(quiz_data.title) free(quiz_data.title); if(quiz_data.version) free(quiz_data.version); if(quiz_data.sub_title) free(quiz_data.sub_title); if(quiz_data.pg_bg_color) free(quiz_data.pg_bg_color); if(quiz_data.pg_bg_color) free(quiz_data.ex_bg_color); if(quiz_data.title_color) free(quiz_data.title_color); if(quiz_data.text_color) free(quiz_data.text_color); if(quiz_data.link_color) free(quiz_data.link_color); if(quiz_data.vlink_color) free(quiz_data.vlink_color); if(quiz_data.font_face) free(quiz_data.font_face); while(quiz_data.head) { qptr = quiz_data.head; quiz_data.head = quiz_data.head->next; free_question_node(qptr); } } static int string_is_empty(char *string) { char *ptr; if(!string) return 1; for(ptr=string; *ptr; ptr++) if(!isspace(*ptr)) return 0; return 1; } /** Removes the question node 'target' *** Returns a pointer to the next answer node after *** the deleted target. **/ QUESTION_NODE *delete_question(QUESTION_NODE *target) { QUESTION_NODE *next_qptr; QUESTION_NODE *qptr; next_qptr = target->next; /* value to be returned */ if(quiz_data.head == target) quiz_data.head = target->next; else { for(qptr = quiz_data.head; qptr && (qptr->next != target); qptr=qptr->next); if(qptr) qptr->next = target->next; } free_question_node(target); return next_qptr; } /** Removes the answer node 'target' from 'question' *** Returns a pointer to the next answer node after *** the deleted target. **/ static ANSWER_NODE *delete_answer(QUESTION_NODE *question, ANSWER_NODE *target) { ANSWER_NODE *next_aptr; ANSWER_NODE *aptr; next_aptr = target->next; /* value to be returned */ if(question->head == target) question->head = target->next; else { for(aptr = question->head; aptr && (aptr->next != target); aptr=aptr->next); if(aptr) aptr->next = target->next; } free_answer_node(target); return next_aptr; } /** Deletes empty answers for question pointed to *** by 'target'. An answer is empty if it has no *** text. **/ static void delete_empty_answers(QUESTION_NODE *target) { ANSWER_NODE *aptr; aptr = target->head; while(aptr) { aptr = string_is_empty(aptr->answer)? delete_answer(target,aptr) : aptr->next; } } /** HotPot 6 (and possibly earlier versions) often stores empty questions *** and answers - questions and answers with no text - in the XML file *** The next function removes all empty questions and answers from the list. *** *** An answer is empty if it has no text. A question is empty if it has *** no text, or if it has no answers **/ static void delete_empty_questions() { QUESTION_NODE *qptr; qptr= quiz_data.head; while(qptr) { delete_empty_answers(qptr); qptr = string_is_empty(qptr->question) || !(qptr->head)? delete_question(qptr) : qptr->next; } } static void set_correct_answer_string(QUESTION_NODE *q_ptr) { ANSWER_NODE *a_ptr; int a; int n_correct; q_ptr->correct_answer_string = (char *)malloc((q_ptr->correct_answer_count + 1) * sizeof(char)); if(!q_ptr->correct_answer_string) cs_critical_error(ERR_MALLOC_FAILED, ""); *(q_ptr->correct_answer_string + q_ptr->correct_answer_count) = '\0'; for(a_ptr = q_ptr->head, n_correct = 0, a=0; a_ptr; a_ptr=a_ptr->next,a++) { if(a_ptr->is_correct) { if(a >= MAX_HOTPOT_CHOICES) /* shared_hotpot_parser.h */ break; *(q_ptr->correct_answer_string + n_correct) = hotpot_answer_letters[a]; /* shared_hotpot_parser.h */ n_correct++; } } } static void set_answer_counts(QUESTION_NODE *q_ptr) { ANSWER_NODE *a_ptr; q_ptr->correct_answer_count = 0; q_ptr->answer_count = 0; for(a_ptr = q_ptr->head; a_ptr; a_ptr=a_ptr->next, q_ptr->answer_count++) if(a_ptr->is_correct) q_ptr->correct_answer_count++; } static void count_answers() { QUESTION_NODE *qptr; for(qptr= quiz_data.head; qptr; qptr=qptr->next) { set_answer_counts(qptr); if( (qptr->question_type == MC) || (qptr->question_type == MC_MULTI)) set_correct_answer_string(qptr); } } int parse_hotpot_file(const char *path) { char *jbc_buf; XML_Parser parser; struct stat buf; FILE *fp; if(stat(path,&buf)) cs_critical_error(ERR_STAT_FAILED,""); jbc_buf = (char *)malloc(buf.st_size); if(!jbc_buf) cs_critical_error(ERR_MALLOC_FAILED,""); fp = fopen(path,"r"); if(!fp) cs_critical_error(ERR_FOPEN_READ_FAILED,""); if(fread(jbc_buf,buf.st_size,1,fp) != 1) cs_critical_error(ERR_FREAD_FAILED, ""); fclose(fp); init_quiz_data(); parser = XML_ParserCreate(NULL); XML_SetElementHandler(parser, startElement, endElement); XML_SetCharacterDataHandler(parser,char_handler); if (!XML_Parse(parser, jbc_buf, buf.st_size, 1)) // cs_critical_error( XML_ErrorString(XML_GetErrorCode(parser)),""); return 1; XML_ParserFree(parser); free(jbc_buf); delete_empty_questions(); count_answers(); return 0; }