\font\ninerm=cmr9 \let\mc=\ninerm % medium caps for names like PASCAL \def\PASCAL{{\mc PASCAL}} \def\mo2{{\mc Modula-2}} \def\[{\ifhmode\ \fi$[\![$} \def\]{$]\!]$\ } \def\<{$\langle\,$} \def\>{$\,\rangle$} \def\sec{{\tensy x}} \def\title{WEBMERGE} \def\contentspagenumber{1} % should be odd \def\topofcontents{\null\vfill \centerline{\titlefont The {\ttitlefont \title} processor} \vskip 15pt \centerline{(Version 1.0)} \vfill} \pageno=\contentspagenumber \advance\pageno by 1 @* Introduction. This program takes a \.{WEB} file and merges multiple change files into it, creating a new \.{WEB} file and a composite change file. If it bears an uncanny resemblance to Donald Knuth's \.{TANGLE}, this is no accident, since it was cloned from \.{TANGLE}. The program uses a few features of the local \mo2\ compiler (Logitech MS-DOS) that may need to be changed in other installations: \yskip\item{1)} Input-output routines may need to be adapted for use with a particular character set and/or for printing messages on the user's terminal. \yskip\noindent System-dependent portions of \.{WEBMERGE} can be identified by looking at the entries for `system dependencies' in the index below. @!@^system dependencies@> The ``banner line'' defined here should be changed whenever \.{WEBMERGE} is modified. @d banner=='This is WEBMERGE, Version 1.0' @ The program begins with a fairly normal header, made up of pieces that @^system dependencies@> will mostly be filled in later. The \.{WEB} input comes from files |web_file| and |change_file| (which is implemented as an array of files), the new \.{WEB} output goes to file |out_web_file|, and the new change file output goes to file |out_change_file|. @p module web_merge; @@; const @@/ type @@/ var @@/ @ This procedure initializes the module @p procedure initialize; var @@/ begin @@/ end initialize; @ Some of this code is optional for use when debugging only; such material is enclosed between the delimiters |debug| and $|gubed|$. Other parts, delimited by |stat| and $|tats|$, are optionally included if statistics about \.{WEBMERGE}'s memory usage are desired. @d debug==@{ (*change this to `$\\{debug}\equiv\null$' when debugging*) @d gubed==@t@>@} (*change this to `$\\{gubed}\equiv\null$' when debugging*) @f debug==begin @f gubed==end @# @d stat==@{ (*change this to `$\\{stat}\equiv\null$' when gathering usage statistics*) @d tats==@t@>@} (*change this to `$\\{tats}\equiv\null$' when gathering usage statistics*) @f stat==begin @f tats==end @ In order to keep this module reasonably free of notations that are uniquely \PASCAL esque, a few macro definitions for low-level output instructions are introduced here. All of the terminal-oriented commands in the remainder of the module will be stated in terms of simple primitives. The boxes signify words that must not be forced to uppercase when the program is \.{MANGLE}d, since \mo2\ is case-sensitive. @^system dependencies@> @d pr_char==@= Write @> (*put a given character into the |output| file*) @d pr_string==@= WriteString @> (*put a given string into the |output| file*) @d rd_string==@= ReadString @> (*read a given string from the |input| file*) @d pr_card==@= WriteCard @> (*put a given cardinal into the |output| file, in decimal notation, using only as many digit positions as necessary*) @d form_feed==@= ff @> (* control code for form feed *) @d new_line==@= WriteLn @> (*advance to a new line in the |output| file*) @d print_string(#)==pr_string(#) (*put a given string into the |output| file*) @d read_string(#)==rd_string(#) (*read a given string from the |input| file*) @d print_cardinal(#)==pr_card(#,1) (*put a given cardinal into the |output| file, in decimal notation, using only as many digit positions as necessary*) @d new_page==pr_char(form_feed) (*advance to a new page in the |output| file*) @d print_ln(#)==pr_string(#); new_line; (*put a given string into the |output| file, followed by a new line *) @d print_char(#)==pr_char(#) (*put a given character into the |output| file*) @= from @= InOut @> import pr_string, rd_string, pr_char, pr_card, new_line; from ascii import form_feed; @ Let's define a few constants. @= @!buf_size=1000; (*maximum length of input line*) @!max_change_files=6; (*maximum number of simultaneous change files*) @!file_name_len=200; (*length of a file name*) @ A global variable called |history| will contain one of four values at the end of every run: |spotless| means that no unusual messages were printed; |harmless_message| means that a message of possible interest was printed but no serious errors were detected; |error_message| means that at least one error was found; |fatal_message| means that the program terminated abnormally. The value of |history| does not influence the behavior of the program; it is simply computed for the convenience of systems that might want to use such information. @d spotless=0 (*|history| value for normal jobs*) @d harmless_message=1 (*|history| value when non-serious info was printed*) @d error_message=2 (*|history| value when an error was noted*) @d fatal_message=3 (*|history| value when we had to stop prematurely*) @# @d mark_harmless==if history=spotless then history:=harmless_message; end; @d mark_error==history:=error_message @d mark_fatal==history:=fatal_message @d err_print(#)==print_ln(#); error; @=@!history:[spotless..fatal_message]; (*how bad was this run?*) @ @=history:=spotless; @ Some implementations may wish to pass the |history| value to the operating system so that it can be used to govern whether or not other programs are started. The system passes a program status value back to DOS. We use |fatal_error| to terminate the program abnormally. @^system dependencies@> @d fatal_error(#)==mark_fatal;print_ln(#); doscall (@"4C,history); @= doscall (@"4C,history); @ If we are going to use |doscall| we have to import it from |system|. @^system dependencies@> @= from system import doscall; @ Syntax errors are reported to the user by saying $$\hbox{`|err_print('! Error message')|'},$$ followed by abnormal termination if no recovery from the error is provided. This will print the error message followed by an indication of where the error was spotted in the source file. Note that no period follows the error message, since the error routine will automatically supply a period. The actual error indications are provided by a procedure called |error|. @p procedure error; (*prints '\..' and location of error message*) var @!k,@!l: [0..buf_size]; (*indices into |buffer|*) begin @; mark_error; end error; @* The character set. One of the main goals in the design of \.{WEB} has been to make it readily portable between a wide variety of computers. Yet \.{WEB} by its very nature must use a greater variety of characters than most computer programs deal with, and character encoding is one of the areas in which existing machines differ most widely from each other. To resolve this problem, all input to \.{WEAVE} and \.{TANGLE} (and \.{WEBMERGE}!) is converted to an internal seven-bit code that is essentially standard ASCII, the ``American Standard Code for Information Interchange.'' The conversion is done immediately when each character is read in. Conversely, characters are converted from ASCII to the user's external representation just before they are output. Such an internal code is relevant to users of \.{WEB} only because it is the code used for preprocessed constants like \.{"A"}. If you are writing a program in \.{WEB} that makes use of such one-character constants, you should convert your input to ASCII form, like \.{WEAVE}, \.{TANGLE} and \.{WEBMERGE} do. Otherwise \.{WEB}'s internal coding scheme does not affect you. @^ASCII code@> This procedure may not be necessary, since virtually all implementations of \mo2\ use ASCII. \mo2\ is much more standardized than \PASCAL. Here is a table of the standard visible ASCII codes: $$\def\:{\char\count255\global\advance\count255 by 1} \count255='40 \vbox{ \hbox{\hbox to 40pt{\it\hfill0\/\hfill}% \hbox to 40pt{\it\hfill1\/\hfill}% \hbox to 40pt{\it\hfill2\/\hfill}% \hbox to 40pt{\it\hfill3\/\hfill}% \hbox to 40pt{\it\hfill4\/\hfill}% \hbox to 40pt{\it\hfill5\/\hfill}% \hbox to 40pt{\it\hfill6\/\hfill}% \hbox to 40pt{\it\hfill7\/\hfill}} \vskip 4pt \hrule \def\^{\vrule height 10.5pt depth 4.5pt} \halign{\hbox to 0pt{\hskip -24pt\O{#0}\hfill}&\^ \hbox to 40pt{\tt\hfill#\hfill\^}& &\hbox to 40pt{\tt\hfill#\hfill\^}\cr 04&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 05&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 06&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 07&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 10&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 11&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 12&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 13&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 14&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 15&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 16&\:&\:&\:&\:&\:&\:&\:&\:\cr\noalign{\hrule} 17&\:&\:&\:&\:&\:&\:&\:\cr} \hrule width 280pt}$$ (Actually, of course, code @'040 is an invisible blank space.) Code @'136 was once an upward arrow (\.{\char'13}), and code @'137 was once a left arrow (\.^^X), in olden times when the first draft of ASCII code was prepared; but \.{WEB} works with today's standard ASCII in which those codes represent circumflex and underline as shown. @= @!ASCII_code=[0..127]; (*seven-bit numbers, a subrange of the integers*) @ The original \PASCAL\ compiler was designed in the late 60s, when six-bit character sets were common, so it did not make provision for lowercase letters. Nowadays, of course, we need to deal with both capital and small letters in a convenient way, so \.{WEB} assumes that it is being used with a \mo2\ whose character set contains at least the characters of standard ASCII as listed above. Some \PASCAL\ compilers use the original name |char| for the data type associated with the characters in text files, while other \PASCAL s consider |char| to be a 64-element subrange of a larger data type that has some other name. In order to accommodate this difference, we shall use the name |text_char| to stand for the data type of the characters in the input and output files. We shall also assume that |text_char| consists of the elements |chr(first_text_char)| through |chr(last_text_char)|, inclusive. The following definitions should be adjusted if necessary. @^system dependencies@> As specified above, this may not be necessary for \mo2, but since this module was mostly cannibalized from \.{TANGLE}, this section was copied too. @d text_char == char (*the data type of characters in text files*) @d first_text_char=0 (*ordinal number of the smallest element of |text_char|*) @d last_text_char=127 (*ordinal number of the largest element of |text_char|*) @ The \.{WEAVE}, \.{TANGLE} and \.{WEBMERGE} processors convert between ASCII code and the user's external character set by means of arrays |xord| and |xchr| that are analogous to \mo2's |ord| and |chr| functions. @= @!xord: array text_char of ASCII_code; (*specifies conversion of input characters*) @!xchr: array ASCII_code of text_char; (*specifies conversion of output characters*) @ If we assume that every system using \.{WEB} is able to read and write the visible characters of standard ASCII (although not necessarily using the ASCII codes to represent them), the following assignment statements initialize most of the |xchr| array properly, without needing any system-dependent changes. For example, the statement \.{xchr[@@\'101]:=\'A\'} that appears in the present \.{WEB} file might be encoded in, say, {\mc EBCDIC} code on the external medium on which it resides, but \.{WEBMERGE} will convert from this external code to ASCII and back again. Therefore the assignment statement \.{XCHR[65]:=\'A\'} will appear in the corresponding \PASCAL\ file, and \PASCAL\ will compile this statement so that |xchr[65]| receives the character \.A in the external (|char|) code. Note that it would be quite incorrect to say \.{xchr[@@\'101]:="A"}, because |"A"| is a constant of type |integer|, not |char|, and because we have $|"A"|=65$ regardless of the external character set. @= xchr[@'40]:=' '; xchr[@'41]:='!'; xchr[@'42]:='"'; xchr[@'43]:='#'; xchr[@'44]:='$'; xchr[@'45]:='%'; xchr[@'46]:='&'; xchr[@'47]:=@="'"@>;@/ xchr[@'50]:='('; xchr[@'51]:=')'; xchr[@'52]:='*'; xchr[@'53]:='+'; xchr[@'54]:=','; xchr[@'55]:='-'; xchr[@'56]:='.'; xchr[@'57]:='/';@/ xchr[@'60]:='0'; xchr[@'61]:='1'; xchr[@'62]:='2'; xchr[@'63]:='3'; xchr[@'64]:='4'; xchr[@'65]:='5'; xchr[@'66]:='6'; xchr[@'67]:='7';@/ xchr[@'70]:='8'; xchr[@'71]:='9'; xchr[@'72]:=':'; xchr[@'73]:=';'; xchr[@'74]:='<'; xchr[@'75]:='='; xchr[@'76]:='>'; xchr[@'77]:='?';@/ xchr[@'100]:='@@'; xchr[@'101]:='A'; xchr[@'102]:='B'; xchr[@'103]:='C'; xchr[@'104]:='D'; xchr[@'105]:='E'; xchr[@'106]:='F'; xchr[@'107]:='G';@/ xchr[@'110]:='H'; xchr[@'111]:='I'; xchr[@'112]:='J'; xchr[@'113]:='K'; xchr[@'114]:='L'; xchr[@'115]:='M'; xchr[@'116]:='N'; xchr[@'117]:='O';@/ xchr[@'120]:='P'; xchr[@'121]:='Q'; xchr[@'122]:='R'; xchr[@'123]:='S'; xchr[@'124]:='T'; xchr[@'125]:='U'; xchr[@'126]:='V'; xchr[@'127]:='W';@/ xchr[@'130]:='X'; xchr[@'131]:='Y'; xchr[@'132]:='Z'; xchr[@'133]:='['; xchr[@'134]:='\'; xchr[@'135]:=']'; xchr[@'136]:='^'; xchr[@'137]:='_';@/ xchr[@'140]:='`'; xchr[@'141]:='a'; xchr[@'142]:='b'; xchr[@'143]:='c'; xchr[@'144]:='d'; xchr[@'145]:='e'; xchr[@'146]:='f'; xchr[@'147]:='g';@/ xchr[@'150]:='h'; xchr[@'151]:='i'; xchr[@'152]:='j'; xchr[@'153]:='k'; xchr[@'154]:='l'; xchr[@'155]:='m'; xchr[@'156]:='n'; xchr[@'157]:='o';@/ xchr[@'160]:='p'; xchr[@'161]:='q'; xchr[@'162]:='r'; xchr[@'163]:='s'; xchr[@'164]:='t'; xchr[@'165]:='u'; xchr[@'166]:='v'; xchr[@'167]:='w';@/ xchr[@'170]:='x'; xchr[@'171]:='y'; xchr[@'172]:='z'; xchr[@'173]:='{'; xchr[@'174]:='|'; xchr[@'175]:='}'; xchr[@'176]:='~';@/ xchr[0]:=' '; xchr[@'177]:=' '; (*these ASCII codes are not used*) @ When we initialize the |xord| array and the remaining parts of |xchr|, it will be convenient to make use of an index variable, |i|. @= @!i:[0..last_text_char]; @ Here now is the system-dependent part of the character set. If \.{WEB} is being implemented on a garden-variety \PASCAL\ for which only standard ASCII codes will appear in the input and output files, you don't need to make any changes here. But at MIT, for example, the code in this module should be changed to $$\hbox{|for i:=1 to @'37 do xchr[i]:=chr(i); end;|}$$ \.{WEB}'s character set is essentially identical to MIT's, even with respect to characters less than @'40. @^system dependencies@> Changes to the present module will make \.{WEB} more friendly on computers that have an extended character set, so that one can type things like \.^^Z\ instead of \.{<>}. If you have an extended set of characters that are easily incorporated into text files, you can assign codes arbitrarily here, giving an |xchr| equivalent to whatever characters the users of \.{WEB} are allowed to have in their input files, provided that unsuitable characters do not correspond to special codes like |carriage_return| that are listed above. (The present file \.{WEBMERGE.WEB} does not contain any of the non-ASCII characters, because it is intended to be used with all implementations of \.{WEB}. It is based on \.{TANGLE}, which was originally created on a Stanford system that has a convenient extended character set, then ``sanitized'' by applying another program that transliterated all of the non-standard characters into standard equivalents.) @= for i:=1 to @'37 do xchr[i]:=' '; end; @ The following system-independent code makes the |xord| array contain a suitable inverse to the information in |xchr|. @= for i:=first_text_char to last_text_char do xord[chr(i)]:=@'40; end; for i:=1 to @'176 do xord[xchr[i]]:=i; end; @* File Handling. @= @!change_file_range= [1..max_change_files]; @ Here we define the symbols for use with file handling. @d lookup_file==@= Lookup @> @d close_file==@= Close @> @d open_input_file(#)== lookup_file(#,filename,false); @d open_output_file(#)== lookup_file(#,filename,true); @d close_input_file(#)== close_file(#); @d close_output_file(#)== close_file(#); @d end_file==@=eof@> @d null_char==@= nul @> @d file_responses==@= Response @> @d end_of_line(#)==(ch = eol) @d end_of_file(#)==(#.end_file) @d read_char==@= ReadChar @> @d write_char==@= WriteChar @> @d input_char(#)==read_char(#,ch); @d output_char(#)==write_char(#,ch) @d read_ln(#)==while not end_of_line(#) do input_char(#); end; @d write_ln(#)==write_char(#,eol); @d text_file==@= File @> @^system dependencies@> @= from @= FileSystem @> import lookup_file,file_responses,read_char,write_char, text_file, close_file; from ascii import eol,null_char; @ Input goes into an array called |buffer|. The two web files, (|web_file| and |out_web_file|), and the output change file (|out_ch_file|), are defined as separate entities. The change files are implememted as an array of files and the symbol |change_file| always refers to the change file which is currently active. @d change_file==change_files[active_change_file] @d changing==(active_change_file > 0) @d change_line==the_change_line[active_change_file] @d change_buffer==the_change_buffer[active_change_file] @d change_file_is_open==the_change_flags[active_change_file] @d change_limit==the_change_limit[active_change_file] @d change_changing== if changing then last_active_change_file := active_change_file; change_line := line; active_change_file := 0; line:=web_line; else active_change_file := last_active_change_file ; web_line:=line; line:=change_line; end; @d output_change_line==output_ln(out_change_file);inc(out_ch_line); @=@!buffer: array[0..buf_size] of ASCII_code; @!web_file,out_web_file, out_change_file : text_file; (* main input web file and the output files*) @!change_files : array change_file_range of text_file; (* the input change files *) @!filename : array [0..file_name_len-1] of char; @!more_names:boolean; (* flag indicating there are more change files*) @ The |input_ln| procedure brings the next line of input from the specified file into the |buffer| array and returns the value |true|, unless the file has already been entirely read, in which case it returns |false|. The conventions of \TeX\ are followed; i.e., |ASCII_code| numbers representing the next line of the file are input into |buffer[0]|, |buffer[1]|, \dots, |buffer[limit-1]|; trailing blanks are ignored; and the global variable |limit| is set to the length of the @^system dependencies@> line. The value of |limit| must be strictly less than |buf_size|. We assume that none of the |ASCII_code| values of |buffer[j]| for |0<=j" " then final_limit:=limit end; if limit=buf_size then while not end_of_line(f) do input_char(f) end; dec(limit); (*keep |buffer[buf_size]| empty*) err_print('! Input line too long'); loc:=0; @.Input line too long@> end; end; read_ln(f); limit:=final_limit; line_pres := true; end; return line_pres ; end input_ln; @ The |output_ln| procedure writes the next line of output from the |buffer| array to the specified file. @p procedure output_ln(var f:text_file); (*outputs a line *) var @!ch : char; (* current output character *) @!temp : [0..buf_size]; begin if limit > 0 then for temp := 0 to limit - 1 do ch := xchr[buffer[temp]]; output_char(f); end; end; write_ln(f); end output_ln; @ In this section we open all of the files. Since we do not know how many change files are to be input (although the maximum is |max_change_files|), we must process them one at a time. @d next_file(#)== filename[0]:= ' '; print_ln(#); read_string(filename); new_line; print_ln(filename); new_line; more_names := (filename[0] <> '!'); @= next_file('web file:'); open_input_file(web_file); next_file('out_web_file:'); open_output_file(out_web_file); next_file('out_change_file:'); open_output_file(out_change_file); more_names:= true; active_change_file := 0; while more_names do @; end; line:=0; web_line:=0;out_web_line:=0;out_ch_line:=0;@/ if active_change_file = 0 then fatal_error('No Change Files specified'); end; for active_change_file:=1 to number_of_actual_change_files do change_line:=0; change_file_is_open := true; prime_the_change_buffer; end; change_changing; limit:=0; loc:=1; buffer[0]:=" "; input_has_ended:=false; @ This section is {\it highly} system-dependent. We have to determine if there are more change files, probably from the command line, and open the next one. If there are no more, we set the variable |more_names| accordingly. @^system dependencies@> @= next_file('change_file:'); more_names := more_names and (active_change_file < max_change_files) ; if more_names then inc(active_change_file); number_of_actual_change_files := active_change_file; open_input_file(change_file); end; @* Change File Handling. @= @!line:cardinal; (*the number of the current line in the current file*) @!web_line:cardinal; (*the number of the current line in the main web file *) @!out_web_line:cardinal; (*the number of the line in the output web file *) @!out_ch_line:cardinal; (*the number of the line in the output change file *) @!the_change_line:array change_file_range of cardinal; (*the number of the current line in the various change files*) @!limit:[0..buf_size]; (*the last character position occupied in the buffer*) @!loc:[0..buf_size]; (*the next character position to be read from the buffer*) @!input_has_ended: boolean; (*if |true|, there is no more input*) @ As we change |changing| from |true| to |false| and back again, we must remember to swap the values of |change_line| and |web_line| so that the |err_print| routine will be sure to report the correct line number. @ When |changing| is |false|, the next line of |change_file| is kept in |change_buffer[0..change_limit]|, for purposes of comparison with the next line of |web_file|. After the change file has been completely input, we set |change_limit:=0|, so that no further matches will be made. @d close_change_file==close_input_file(change_file); change_file_is_open := false; @= @!the_change_buffer:array change_file_range of array[0..buf_size] of ASCII_code; @!the_change_limit:array change_file_range of [0..buf_size]; (*the last position occupied in |change_buffer|*) @!the_change_flags:array change_file_range of boolean; (* flag indicating change file is still open *) @!active_change_file : [0..max_change_files]; (* |change_file_range| plus 0 to indicate the main web file *) @!last_active_change_file,@!number_of_actual_change_files : change_file_range; (* change file indexes *) @ Here's a simple function procedure that checks if the two buffers are different. @p procedure lines_dont_match():boolean; var @!k:[0..buf_size]; (*index into the buffers*) begin if (change_limit<>limit) or (not change_file_is_open) then return true; end; if limit>0 then for k:=0 to limit-1 do if change_buffer[k]<>buffer[k] then return true; end; end; end; return false; end lines_dont_match; @ Procedure |prime_the_change_buffer| sets |change_buffer| in preparation for the next matching operation. Blank lines in the change file are not used for matching. This procedure is called only when |changing| is true; hence error messages will be reported correctly. @p procedure prime_the_change_buffer; var @!k:[0..buf_size]; (*index into the buffers*) begin change_limit:=0; (*this value will be used if the change file ends*) @; @; @; end prime_the_change_buffer; @ While looking for a line that begins with \.{@@x} in the change file, we allow lines that begin with \.{@@}, as long as they don't begin with \.{@@y} or \.{@@z} (which would probably indicate that the change file is fouled up). @d lowercasify==if (buffer[1]>="X")and(buffer[1]<="Z") then buffer[1]:=buffer[1]+"z"-"Z"; end; @= loop inc(change_line); if not input_ln(change_file) then close_change_file;return; elsif (limit>=2) and (buffer[0]="@@") then lowercasify; if buffer[1]="x" then exit; elsif (buffer[1]="y")or(buffer[1]="z") then loc:=2; err_print('! Where is the matching @@x?'); end; end; end; @ Here we are looking at lines following the \.{@@x}. @= repeat inc(change_line); if not input_ln(change_file) then err_print('! Change file ended after @@x'); close_change_file; @.Change file ended...@> return; end; until limit>0; @ @= change_limit:=limit; if limit>0 then for k:=0 to limit-1 do change_buffer[k]:=buffer[k]; end; end; @ The following procedure is used to see if the next change entry should go into effect for any of the change files; it is called only when |changing| is false. The idea is to test whether or not the current contents of |buffer| matches the current contents of the |change_buffer| for one of the change files. If none match, there's nothing more to do; but if so, a change is called for: All of the text down to the \.{@@y} is supposed to match. An error message is issued if any discrepancy is found. Then the procedure prepares to read the next line from the selected |change_file|. @p procedure check_change; (*switches to |change_file| if the buffers match*) var @!n:cardinal; (*the number of discrepancies found*) @!k:[0..buf_size]; (*index into the buffers*) @!none_match:boolean; (*none of the change files match*) @!temp_index : [1..max_change_files+1]; @!prev_history:[spotless..fatal_message]; (* previous value of |history| *) @!flush_needed:array change_file_range of boolean; (* flags indicating a particular change file must be flushed because of overlap *) begin @; if none_match then return; end; n:=0; @; loop @; change_changing; (*now it's |true|*) inc(line); if not input_ln(change_file) then err_print('! Change file ended before @@y'); @.Change file ended...@> change_limit:=0; change_changing; (*|false| again*) return; end; @; @; change_changing; (*now it's |false|*) inc(line); if not input_ln(web_file) then err_print('! WEB file ended during a change'); @.WEB file ended...@> input_has_ended:=true; return; end; change_changing; (*now it's |true|*) if lines_dont_match() then inc(n) else output_change_line; end; change_changing; (*now it's |false|*) end; end check_change; @ If any other change files match the same line as the currently active change file, they must be flushed, since \.{WEBMERGE} does not allow overlapping change files. We make two passes through the list of change files because the call to |prime_the_change_buffer| if a flush occurs will modify |buffer| and interfere with further comparisons. @d this_line_matches==(not lines_dont_match() ) @= for active_change_file := 1 to number_of_actual_change_files do flush_needed[active_change_file] := (active_change_file <> last_active_change_file) and this_line_matches ; if flush_needed[active_change_file] then prev_history := history; print_string('overlapping change files --- '); print_cardinal(last_active_change_file); print_string(' and '); print_cardinal(active_change_file); print_string(' --- '); print_cardinal(active_change_file); err_print(' flushed'); history := prev_history; mark_harmless; end; end; for active_change_file := 1 to number_of_actual_change_files do if flush_needed[active_change_file] then @; prime_the_change_buffer; end; end; active_change_file := 0; @ We flush the aborted change by skipping to the next \.{@@x}. This procedure is almost the same as the one to skip over comment lines, except that we {\it expect} to hit the \.{@@y} and the \.{@@z}, do not want the error messages, and we want to |exit| rather than |return| if end-of-file is encountered. @= loop inc(change_line); if not input_ln(change_file) then close_change_file;exit; elsif (limit>=2) and (buffer[0]="@@") then lowercasify; if buffer[1]="z" then exit; end; end; end; @ We are going to compare the target line with each of the active change files. The files are searched in ascending order. The first one to match becomes the active change file. @d one_matches==(not none_match) @= none_match := true; temp_index := 1; repeat if (temp_index <= number_of_actual_change_files ) then active_change_file := temp_index ; end; inc(temp_index); none_match := none_match and lines_dont_match(); until one_matches or (temp_index > number_of_actual_change_files) ; last_active_change_file := active_change_file ; active_change_file := 0; @ @= change_changing; (*now it's |true|*) buffer[0]:="@@"; buffer[1]:="x"; limit:=2; output_change_line; limit:=change_limit; buffer[0]:=change_buffer[0]; buffer[1]:=change_buffer[1]; change_changing; (*now it's |false|*) output_change_line; @ @= if (limit>1) and (buffer[0]="@@") then lowercasify; if (buffer[1]="x")or(buffer[1]="z") then loc:=2; err_print('! Where is the matching @@y?'); @.Where is the match...@> elsif buffer[1]="y" then output_change_line; if n>0 then loc:=2; print_string('! Hmm... '); print_cardinal(n); err_print(' of the preceding lines failed to match'); @.Hmm... n of the preceding...@> end; return; end; end; @* Main Input Loop. @= while not input_has_ended do get_line; output_ln(out_web_file); inc(out_web_line); if (out_web_line mod 100) = 0 then print_char('.'); if (out_web_line mod 2000) = 0 then new_line; end; end; if changing then output_change_line; end; end; @ The |get_line| procedure is called to put the next line of merged input into the buffer and update the other variables appropriately. A space is placed at the right end of the line. @p procedure get_line; (*inputs the next line*) var @!previous_change_mode : boolean; begin repeat previous_change_mode := changing; if changing then @; else @; end; until previous_change_mode = changing; loc:=0; buffer[limit]:=" "; end get_line; @ @= inc(line); if not input_ln(web_file) then input_has_ended:=true else check_change; end; @ @= inc(line); if not input_ln(change_file) then err_print('! Change file ended without @@z'); @.Change file ended...@> buffer[0]:="@@"; buffer[1]:="z"; limit:=2; end; if limit>1 then (*check if the change has ended*) if buffer[0]="@@" then lowercasify; if (buffer[1]="x")or(buffer[1]="y") then loc:=2; err_print('! Where is the matching @@z?'); @.Where is the match...@> elsif buffer[1]="z" then output_change_line; limit := 0; output_change_line; change_changing; active_change_file := last_active_change_file; prime_the_change_buffer; active_change_file := 0; end; end; end; @ At the end of the program, we will tell the user if the change file had a line that didn't match any relevant line in |web_file|. @= close_input_file(web_file); close_output_file(out_web_file); close_output_file(out_change_file); for active_change_file:=1 to number_of_actual_change_files do if change_file_is_open then close_change_file; if change_limit<>0 then (*|changing| is false*) for loc:=0 to change_limit do buffer[loc]:=change_buffer[loc]; end; limit:=change_limit; line:=web_line; loc:=change_limit; err_print('! Change file entry did not match'); @.Change file entry did not match@> end; end; end; @ The error locations can be indicated by using the global variables |loc|, |line|, and |changing|, which tell respectively the first unlooked-at position in |buffer|, the current line number, and whether or not the current line is from |change_file| or |web_file|. This routine should be modified on systems whose standard text editor has special line-numbering conventions. @^system dependencies@> @d tab_mark=9 @= if changing then print_string('. (change file '); print_cardinal(active_change_file); print_char(' '); else print_string('. ('); end; print_string('l.'); print_cardinal(line); print_ln(')'); if loc>=limit then l:=limit else l:=loc; end; for k:=1 to l do if buffer[k-1]=tab_mark then print_char(' ') else print_char(xchr[buffer[k-1]]); end; (*print the characters already read*) end; new_line; for k:=1 to l do print_char(' '); end; (*space out the next line*) for k:=l+1 to limit do print_char(xchr[buffer[k-1]]);end; (*print the part not yet read*) new_line; (*this separates the message from future output*) @ Here we simply report the history to the user. @= case history of spotless: print_ln('(No errors were found.)')| harmless_message: print_ln('(Did you see the warning message above?)')| error_message: print_ln('(Pardon me, but I think I spotted something wrong.)')| fatal_message: print_ln('(That was a fatal error, my friend.)') end; (*there are no other cases*) @ @= stat new_line; print_ln('Line count statistics:'); print_cardinal(web_line); print_ln(' lines in input web file'); for active_change_file := 1 to number_of_actual_change_files do print_cardinal(change_line); print_string(' lines in change file '); print_cardinal(active_change_file); new_line; end; print_cardinal(out_web_line); print_ln(' lines in output web file'); print_cardinal(out_ch_line); print_ln(' lines in output change file'); tats @* Main Program. This is the main program. @p begin initialize; print_ln(banner); @; @; @; @; @; @; end web_merge. @* Index.