#!/usr/local/bin/perl -s # # This is mklesson, a `lesson compiler.' # Mklesson takes a lesson file (.les) as its sole argument, and uses it to # generate a set of hypertext tutor files in HTML (.html) format. # The generated files are created in the current directory. # Status information is sent to standard output (which is usually the screen). # # Usage Example: # mklesson lesson9.les # # Options: # -a alphabetic filenames (use "s" instead of "-" in # generated filenames) # -b book # -d define (not implemented) # -s short filenames (.htm instead of .html) # # If you're generating ISO 9660 CD-ROMS, use "-s -a". # # $Id: mklesson,v 1.15 1995/08/09 21:07:40 wheeler Exp $ # # See the user's guide in file userg.html and formatmk.txt. # # Input: # (argument 1) -- filename of lesson (.les) file. # template -- file with templates used for file generation. # default -- if this file exists, it is read to find default settings # (its format is identical to a lesson header section). # # # Output: The program generates files with the following naming patterns: # sLESSON-SECTION.html - text for lesson number LESSON, section SECTION. # If this is the last one, SECTION is "f" (final) # so that the next lesson can always link # backwards correctly (didn't use "l" for last, # because it looks too much like a "1"). # sLESSON-SECTIONrRESPONSENUMBER.html - text for response RESPONSENUMBER # lessonLESSON.html - lesson outline. # # With the "-a" option, the dashes above become "s". # # # This program is written in the programming language perl because it's # a relatively short program that performs text processing (a perl strength), # and I wanted it to be VERY portable (perl is widely available). # Perl has some disadvantages, in particular, please be careful modifying # this program because the Perl language syntax is so awful that it's # as though it was designed to maximize error creation. # # Copyright (C) 1994 David A. Wheeler. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # Since mklesson is free software, if you modify this program and # then distribute it (or results generated from it) you ARE REQUIRED # to distribute & permit anyone to copy the modified version of this program. # # You CAN use this program to generate # proprietary tutorials, just as you can use gcc to generate # proprietary executables. See the GPL for more information. # START OF MAIN PROGRAM if (defined($s)) { # a "-s" option uses Short extension names (.htm). $html_extension = ".htm"; } else { # without a "-s" option, generate ".html" extensions. $html_extension = ".html"; } # This version has primitive support for preprocessing commands of form: # # [ ... ] # # In the long term I'd like to support arbitrary preprocessing "define" # flags, but for now there only one flag, "BOOK". # You can set "BOOK" with the "-b" flag. # Eventually there will be another parameter to set arbitrary flags. # -b ("book") option. $is_book = 0; if (defined($b)) { $is_book = 1; } # book_state has one of three values: # 0 = normal text. # 1 = inside an . Print only if $is_book. # 2 = inside an of . Print only if not $is_book. $book_state = 0; # -a ("alphabetic") option. $alphabetic = 0; $separator= '-'; if (defined($a)) { $alphabetic = 1; $separator= 's'; } &initialize_variables; &read_in_template; # Load the template file into memory. &read_in_defaults; # Load the contents of file "default". # Open & process the lesson file. $lesson_source=shift; &open_lesson_file; &process_input_header; &process_input_body; &change_timestamp_file($lesson_source); &move_in_new_files; # END OF MAIN PROGRAM # SUBROUTINES: sub process_input_header { # Process input file as a lesson header section. # Stop processing at end-of-file or a
marker. # (The input file could be a "lesson" file or "default" file) while (&read_line) { if (m/^ *$/i) { # Note: this format is slightly different, grabbing the ENTIRE line. # This is so that the address can include anchors and such, which # require the use of " and > characters. $author_address=$1; print "Author address is $author_address\n"; } elsif (m/^ *${lesson_outline}.new")) {die "Sorry, cannot open output lesson outline file ${lesson_outline}"}; &substitute_and_append("outline.head", "outline"); # Process body of .les file. while ($_ ne "") { # While there's still some text to process. if (m/^
; get its name. $title= $_; $title =~ s/^
${section_filename}.new")) {die "Sorry, cannot open section file ${section_filename}"}; $section_file_open = 1; &determine_next_section_URL; &determine_previous_section_URL; $section_question_URL = "s${lesson_number}${separator}" . "${section_number_file_rep}" . $html_extension . "#quiz"; $section_has_question = 0; # We haven't seen section question yet. print OUTLINE '
  • ', ${title}, "\n"; &reset_set_response; # Insert the section header. &substitute_and_append("section.head", "section"); while (!eof(IN)) { &read_line; # Is this the start of a question? if (m/^]/i) {&process_question;}; # Is this the end of the section? if (m/^
    ]*> *//i; chop; if ($_ ne "") {print SECTION "$_\n"}; # Copy text until there's a marker. while (!eof(IN)) { &read_line; if (/^. Sets $_ to its new value, and returns it. # Discards lines, processes TEXT lines. # Primitively handles IFDEF. # This recurses, which is less efficient but who cares. $_ = ; if (/^ *. Print only if $is_book. # 2 = inside an of . Print only if not $is_book. if (/^ */i) {$book_state = 1; &read_line}; if (/^ */i) { die "ELSE but not in an IFDEF BOOK!" if $book_state == 0; if ($book_state == 1) {$book_state = 2;} else {$book_state = 1;} &read_line; } if (/^ */i) {$book_state = 0; &read_line}; if (/^ *) { $count += 1;} close(COUNTFILE); return $count; } sub read_textfile { # Read in an entire non-HTML text file. # Sets $_ to the file contents, and returns $_'s value. # Performs substitutions so that "special" HTML characters # come out correctly (&, <, etc.) my $lines_in_textfile = 0; my $lines_plus_one_in_textfile = $lines_in_textfile + 1; my $template_name = ""; my $temporary = ""; m/^ * ]*)/i) { $template_name = $1; $lines_in_textfile = &count_lines_in_file($text_filename); $lines_plus_one_in_textfile = $lines_in_textfile + 1; } if (m/FONT=PRE/i) {$use_pre=1} else {$use_pre=0}; $file_contents = ""; if ($use_pre) {$file_contents .= "
    \n"};
    
      # This is kludgy; the substitutes are repeated below, and other
      # chunk substitutes are supported.  In the long term, this should
      # all be replaced with a nice simple substitution system.
      if ($template_name) {
          $temporary = $template{ $template_name . ".head" };
          $temporary =~ s/\[INCLUDED_FILENAME\]/$text_filename/gm;
          $temporary =~ s/\[LINES_IN_INCLUDED_FILE\]/$lines_in_textfile/gm;
          $temporary =~ s/\[LINES_PLUS_ONE_IN_INCLUDED_FILE\]/$lines_plus_one_in_textfile/gm;
          $file_contents .= $temporary;
      }
    
      print "  Incorporating text (non-HTML) file ${text_filename}.\n";
      if (!open(TEXTFILE, "<${text_filename}"))
        {die "Sorry, cannot open text file ${text_filename}"};
      while () {
        # Substitute special HTML characters.
        s/\&/&/g;   # & becomes &
        s/\/>/g;    # > becomes >
        # Do _NOT_ substitute the double-quote character (" into ")
        # Windows NCSA Mosaic version 2.0 alpha 2 doesn't handle it,
        # and all the browsers I can find can display them quite well.
        #  s/\"/"/g;  # " becomes "
        $file_contents .= $_;  # Add the contents.
      }
      close(TEXTFILE);
      if ($template_name) {
          $temporary = $template{ $template_name . ".tail" };
          $temporary =~ s/\[INCLUDED_FILENAME\]/$text_filename/gm;
          $temporary =~ s/\[LINES_IN_INCLUDED_FILE\]/$lines_in_textfile/gm;
          $temporary =~ s/\[LINES_PLUS_ONE_IN_INCLUDED_FILE\]/$lines_plus_one_in_textfile/gm;
          $file_contents .= $temporary;
      }
      if ($use_pre) {$file_contents .= "
    \n"}; $_ = $file_contents; # Set the value, and return it. } sub process_choices { # Process the choices portion of a question in a section. # It must be of the form: # # Text_IO # # # Note: the numbers in the item are ignored. die "CHOICES section expected." unless /^\n"; $choice_number = 0; while (!eof(IN)) { &read_line; if (/^<\/CHOICES/) {last}; if (/^]*> *//i; chop; print SECTION '
  • "; print SECTION $_; print SECTION "\n"; } else {die "Sorry, choice text must be on the same line as ."} } $number_of_choices = $choice_number; die "A Choice section must end with " unless (/^<\/CHOICES>/i); &read_line; if (/^/i) { $correct_answer = $1 + 0; } else { die "A Choice section must be followed with the answer, " . "format: "; } &read_line; # Get the next line for the callee. &substitute_and_append("section.question.tail", "section"); } sub process_optional_responses { # Process the optional response portion of a section: # # ..text.. # # A
    could also follow. # Skip blank lines, if any. while ($_ || !eof(IN)) { if (//i) {last}; if (/
    /i) { # We have a response section. $current_response = 0; while (&read_line) { if (/<\/RESPONSES>/i) {&read_line; last}; if (//i) { &close_response_if_necessary; if (//i) { $current_response = $_; $current_response =~ s/^.*$/$1/i; chop($current_response); } else { if (//i) {$current_response = $correct_answer} else {die "Invalid WHEN clause"} }; $set_response[$current_response] = 1; # Mark this response as found &open_response; # Insert response.correct.head or response.incorrect.head if ($current_response == $correct_answer) { &substitute_and_append("response.correct.head", "response"); } else { &substitute_and_append("response.incorrect.head", "response"); } # If there's text on the same line as the WHEN clause, put it # in the response file. s/^ *]*> *//i; chop; if ($_ ne "") {print RESPONSE "$_\n"}; } else { # We got some text, presumably part of a response. if ($current_response == 0) { print "Warning: Responses not associated with a specific answer\n"; print "Text is: $_"; } else { print RESPONSE; } } } &close_response_if_necessary; } } sub open_response { # Open the response file & set the variable names referring to it. $response_filename="s${lesson_number}${separator}${section_number_file_rep}" . "r${current_response}${html_extension}"; if (!open(RESPONSE, ">${response_filename}.new")) {die "Sorry, cannot open response file ${response_filename}.new"}; print " Opened response file ${response_filename}\n"; $response_file_open = 1; &substitute_and_append("response.head", "response"); } sub close_response_if_necessary { if ($response_file_open == 1) { if ($current_response == $correct_answer) {&substitute_and_append("response.correct.tail", "response");} else {&substitute_and_append("response.incorrect.tail", "response");}; &substitute_and_append("response.tail", "response"); close(RESPONSE); $response_file_open = 0; } } sub generate_default_responses { $current_response = 1; while ($current_response <= $number_of_choices) { if ($set_response[$current_response] == 0) { &open_response; # insert default text. if ($current_response == $correct_answer) { &substitute_and_append("response.correct.head", "response"); &substitute_and_append("response.correct.default", "response"); } else { &substitute_and_append("response.incorrect.head", "response"); &substitute_and_append("response.incorrect.default", "response"); } &close_response_if_necessary; } $current_response++; } } sub determine_next_section_URL { # Set $next_section_URL, $confirm_skip_to_next_section_URL if ($section_number == $number_of_sections) { $next_prefix = $next_lesson_location; $next_lesson_number = $lesson_number + 1; $next_section_file_rep = "1"; } else { $next_prefix = ""; $next_lesson_number = $lesson_number; $next_section_file_rep = $section_number + 1; if ($next_section_file_rep == $number_of_sections) {$next_section_file_rep = "f"}; } $next_section_URL = "${next_prefix}s${next_lesson_number}" . "${separator}${next_section_file_rep}${html_extension}"; # Set $confirm_skip_to_next_section_URL, the URL to go to if # the user selects "skip" from a section. if ($lesson_number == 1 && $section_number == 1) { # For lesson 1, section 1, confirm the skipping of the quiz # by linking to the special file "skip1-1.html". $confirm_skip_section_URL = 'skip1' . $separator . '1' . $html_extension; } else { $confirm_skip_section_URL = $next_section_URL; }; } sub determine_previous_section_URL { # Determine & set the value of: # $previous_section_URL # $the_previous_section_or_home $the_previous_section_or_home = $template{"the_previous_section_is_not_home"}; if ( $the_previous_section_or_home == "" ) { $the_previous_section_or_home = "the previous section" }; local($template_text) = $template{ $template_chunk }; if ($section_number == 1) { $previous_lesson_number = $lesson_number - 1; if ($lesson_number == 1) { # lesson 1, section 1 - the "previous" section is the home page. $previous_section_URL= $tutor_home_page_URL; $the_previous_section_or_home = $template{"the_previous_section_is_home"}; if ( $the_previous_section_or_home == "" ) { $the_previous_section_or_home = "the tutorial home page" }; } elsif ($previous_lesson_location_set == 1) { $previous_section_URL= $previous_lesson_location . '/' . 's' . $previous_lesson_number . $separator . 'f' . $html_extension; } else { # No previous location set; must be current directory. $previous_section_URL= 's' . $previous_lesson_number . $separator . 'f' . $html_extension; } } else { $previous_lesson_number = $lesson_number; $previous_section_number = $section_number - 1; $previous_section_URL= 's' . $previous_lesson_number . $separator . $previous_section_number . $html_extension; } } sub move_in_new_files { # Move in all files with .new extensions to replace original files. # If desired, this can be modified so it only replaces "changed" files. print "\n"; print "Replacing changed files, if any.\n"; while (<*.new>) { $oldname = $_; $newname = $_; $newname =~ s/\.new$//; if (system("cmp -s $oldname $newname") != 0) { # The new file is different, remove the older one. print " Replacing $newname (with $oldname)\n"; rename($newname, "${newname}.BAK"); # Keep older one, just in case. rename($oldname, $newname); } else { unlink($oldname); } } } sub change_timestamp_file { # Change the timestamp file. Parameter 1 is the input filename. # The timestamp is used so we'll know when this file was last generated. local($infile_name) = @_; $timestamp_filename = $infile_name; $timestamp_filename =~ s/\.les$//; $timestamp_filename .= ".tim"; if (!open(TIMESTAMP_FILE, ">${timestamp_filename}")) {die "Sorry, cannot create timestamp file ${timestamp_filename}"}; print TIMESTAMP_FILE "Lesson Generated.\n"; close(TIMESTAMP_FILE); } sub read_in_template { # Read in file "template", putting results in associative array %template local($current_template_unit); $current_template_unit = "garbage"; if (!open(TEMPLATE, ") { if (/^===== (\S+) =====/) {$current_template_unit = $1;} elsif ($current_template_unit eq "comment") {} # Do nothing with comments. elsif ($current_template_unit eq "garbage") {print "WARNING: Garbage line in file template: $_";} else { $template{$current_template_unit} .= $_; } # Append to assoc array. }; close(TEMPLATE); # If debug on, print out the %template contents. if ($debug_template == 1) { print "DEBUG: Printing out template contents as read:\n"; foreach $i (%template) { print "=====\n"; print $i; print "\n"; } } } sub reset_set_response { # Reset array "set_response", which stores the list of responses # set in this section by the user. This is used to determine # which responses should have "default" responses set for them. # Assume no more than 9 responses. @set_response = ( 0, 0, 0, 0, 0, 0, 0, 0, 0 ); $choice_number = 0; } sub initialize_variables { # Note - these are GLOBAL variables. $lesson_number = 0; $section_number = 0; $tutor_name=""; $author_name=""; $author_email=""; $author_address=""; $section_has_question = 0; # Does this section have a question? 0=false. $previous_lesson_location_set = 0; $previous_lesson_location =""; $next_lesson_location_set = 0; $next_lesson_location =""; $master_outline_set = 0; $master_outline_URL =""; $tutor_home_page_set = 0; $tutor_home_page_URL = ""; $section_file_open = 0; # 0=false, 1=true. $response_file_open = 0; $current_response = 0; # Determine today's date and year. ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime(time); $mon++; # Month was returned as 0-11, so we need to fix it. # Determine 4-digit year; recode in 2090. if ($year < 90) { $year += 2000 } else { $year += 1900 }; $todays_date = $year . "." . $mon . "." . $mday . " (YY.MM.DD)"; $todays_year = $year; print "Today's date is ${todays_date}; year is $todays_year\n"; } sub open_lesson_file { # Get lesson file name and find out how many sections it has. if (!open(IN, "<$lesson_source")) {die "Sorry, cannot open input file ${lesson_source}"}; $number_of_sections=0; while () { if (m/^
    # ?? Clean up code (use perl shortcuts, examine for EOF). # ?? Error check input. For head, warn of unused. # ?? More error checking in body processing.