% Version 1.3 makes it easier to create other changes files (6/89) \font\twelvept=cmbx12 \font\tentex=cmr10 \def\topofcontents{\null\vfill\eject \def\titlepage{T} \centerline{{\twelvept The \TeX IX Index Program}} \centerline{{\twelvept For IBM VM/CMS Pascal/VS}} \vskip15pt \centerline{Version 1.3, June 1989} \hbox{\vbox{\hsize\the\hsize This work is protected as an unpublished work under U.S. copyright laws. Copyright $\copyright$ 1988 by WSUCSC. All rights Reserved.}} \vskip18pt \hbox{\vbox{\hsize\the\hsize This software is furnished under a license for use only on a single computer system and may be copied only with the inclusion of the above copyright notice. This software, or any other copies thereof, may not be provided or otherwise made available to any other person except for use on such system and to one who agrees to these license terms. Title to and ownership of the software shall at all times remain in WSUCSC.}} \vfill} \let\tentex=\tt \def\_{\leavevmode \kern.06em \vbox{\hrule width.3em}}% % \def\}{\ifmmode \rbrace \else $\rbrace$\fi}% % \def\{{\ifmmode \lbrace \else $\lbrace$\fi}% % \def\us#1{$\underline{\smash{\hbox{#1}}}$}% \def\lin#1{\par \leftskip 0pt \advance \leftskip by #1 }% \def\vs#1{\vskip #1\relax} \catcode`*=11 % labeled definition macro % \newdimen\la*w \la*w=1in % Label width \newtoks\la*g \la*g={1em} % Label gutter \newtoks\la*s \la*s={12pt} % Skip before label \newtoks\la*f \la*f={\rm} % Label font \newbox\tempbox % \long\def\label#1{\par\vskip\the\la*s \setbox\tempbox=\vtop{\hsize=\la*w \leftskip=0pt \rightskip=0pt plus2em \tolerance=1600 \noindent \the\la*f #1}% \hangindent=\la*w \advance \hangindent by \the\la*g \hangafter=1 \noindent \setbox0=\hbox{\rlap{\box\tempbox}}\ht0=0pt\dp0=0pt\box0% \hskip\la*w \hskip\the\la*g \ignorespaces} \let\la=\label \catcode`*=12 \let\nin=\noindent \def\lbr{\null\hfil\break} \def\PASCAL{{\sc PASCAL/VS}} \def\hs#1{\hskip#1} \def\text#1{{\it \TeX T#1}} \def\9{\it} \def\bs{$\backslash$} % Version 1.0; Dean Guenther; 4/1/86 % @* Introduction. This procedure was created to give \text1\ the ability to produce a sorted index in the same one pass, instead of creating the index, sorting it, and printing the index out in three separate steps. To accomplish this feat, a new control sequence, \bs{}sortindex, was added to the basic \TeX\ program which gives the \text1\ user up to three sorted index files per run. The format of this new control sequence is \par{\tt\indent\bs{}sortindex\it n} \par \nin where {\it n} is the index file number: 1, 2, or 3. @ The @^TEXIX@> program is written entirely in WEB, except for an external procedure, {\it plsort}, which is written in @^PL/1@> PL/1. This was necessary since \PASCAL\ cannot call @:plsort@> Syncsort @^SYNCSORT@> to do an internal sort, and PL/1 can. @ Also, it should be pointed out early that I made use of \PASCAL' nifty string handling capability, in particular, the |string|, |index| and |substr| functions. If needed, these should not be too difficult to translate into another \PASCAL\ compiler (famous last words). @p segment texix; procedure texix(ix:char); external; procedure texix; const @ type @ var @! m,k,i,j :integer;@/ @ @ The file {\it ix\_file} is the file opened for output in the \text1\ session itself. It will be closed in TEXIX, opened for input, closed again, and finally opened for output where the final sorted index file will be written to, complete with all \text1\ markup necessary for printing the index. @= sysprint:text; @!ix_file :text; @ The {\it sort\_file} is used for the internal sort only. The \text1\ user never has access to it. @= @!sort_file :file of sort_type; @* Macros and definitions. Here are some macros and definitions used throughout. @d incr(#) == # := # + 1 @d decr(#) == # := # - 1 @d othercases == otherwise @d endcases == end; @d dosubstr == substr @d doindex == index @f othercases == else @f endcases == end @d getout == return @d messages == sysprint @d max_field = 300 @d max_levels = 3 @d max_lrecl = 2048 @d remove_characters(#) == if length(in_record) > # then in_record := dosubstr(in_record,1 + #) else in_record := '' @d do_nothing == begin end @d do_sort_as == begin remove_characters(6); i := doindex(in_record,'{$}'); write_sort_chars(i-1); remove_characters(3); end @d check_case == begin if not respect_case then for j:= 1 to sort_part[i].field_lngth do sort_part[i].field_level[j] := upper_case(sort_part[i].field_level[j]); end @d string_type(#) == string(#) @d ccat_temp == @t\hs{2em}@>@=||@>@t\hs{2em}@> @d add_comma(#) == # := ccat(#,',') @d add_cmma_blnk(#) == # := ccat(#,', ') @d do_when_bold == begin if length(bold_string) > 0 then add_comma(bold_string); bold_string:= ccat(ccat(ccat(bold_string,'{\bd '),curr_str_page),'}'); @.\bs{}bd@> end @d do_when_underscore == begin if length(us_string) > 0 then add_comma(us_string); us_string:= ccat(ccat(ccat(us_string,'\us{'),curr_str_page),'}'); @.\bs{}us@> end @d do_when_italic == begin if length(rm_string) > 0 then add_comma(rm_string); rm_string:= ccat(ccat(ccat(rm_string,'{\it '),curr_str_page),'}'); @.\bs{}it@> end @d do_when_roman == begin if length(rm_string) > 0 then add_comma(rm_string); rm_string:=ccat(rm_string,curr_str_page); end @d do_write(#)==write(#) @d do_lnwrite(#)==writeln(#) @ This is arbitrary, but there should never be more than 8 indicies. @d s_file_number == '9' @ This is used to print the subentry1 header if there are no page numbers under its subentry and there are under subentry2. @d id2_missing_check== curr_level:=2; if sort_record.sort_part[3].field_lngth=0 then do_nothing else begin id2_is_missing:=true; write_header(sort_record.print[2].field_lngth, sort_record.print[2].field_level); id2_is_missing:=false; curr_level:=3;end @ This is used to print the primary header if there are no page numbers under the primary header, and there are page numbers under either subentry1 or subentry2. @d id1_missing_check==if sort_record.sort_part[2].field_lngth = 0 then begin curr_level:=1; if sort_record.sort_part[3].field_lngth = 0 then do_nothing else begin id1_is_missing:=true; write_header(sort_record.print[1].field_lngth, sort_record.print[1].field_level); id1_is_missing:=false; curr_level:=3;end;end else begin curr_level:=1; id1_is_missing:=true; write_header(sort_record.print[1].field_lngth, sort_record.print[1].field_level); id1_is_missing:=false; id2_missing_check; end @ This is a little macro used to access {\it write\_header}. @d write_prev_header== begin case curr_level of 1: write_header(prev_ln1,prev_pn1); 2: write_header(prev_ln2,prev_pn2); 3: write_header(prev_ln3,prev_pn3); othercases do_nothing endcases end; @ The function ccat will concatenate two strings together. @^System dependent code@> @p function ccat(x,y:string_pass):string_pass; begin ccat := x ccat_temp y; end; @* Opening and Closing the files. This is all very dependent on \PASCAL. @ The {\it reset\_file} procedure is used to open all files. One time this is done when first entering TEXIX. Note that the file name is ``{\tt IX\it n}'' where ``{\it n}'' is 1--3. This can easily be extended to 4--8 by chaning the \bs{}index markup in TEXT1@@. The other time the open is done is when opening the sort file, after it has already been sorted. This is system dependent. The following will work for \PASCAL. @^System dependent code@> @p procedure reset_file(file_number:char); begin if file_number = '9' then reset(sort_file, 'NAME=TEXT1$$.OUTSORT.A,LRECL=1857,RECFM=V') else reset(ix_file, ccat(ccat('NAME=TEXT1$$.IX',str(file_number)),'.A')); end; @ The {\it sort\_file} is opened for output to write the sort records to. The {\it ix\_file} is opened for output after the sort records have been sorted and processed again. This is system dependent. The following will work for \PASCAL. @^System dependent code@> @p procedure file_rewrite(file_number:char); begin if file_number = '9' then rewrite(sort_file,'NAME=TEXT1$$.INSORT.A,LRECL=1857,RECFM=V') else rewrite(ix_file, ccat(ccat('NAME=TEXT1$$.IX',str(file_number)), '.A,LRECL=2048,RECFM=V')); end; @* The Sort Record Description. The record type called {\it sort\_type} is the record written to the {\it sort\_file}. If consists of the following: \la{\bf Bytes} \la{1} A one byte {\it record\_type} (0={\bf bold page number}; 2= roman page number (the default); 4=\us{underscored} \us{page} \us{number}; 6={\it italic page number}; 9=blind entry). \la{2--5} The integer page number. If the page number is in the preface part of the document, then the page number will be negative. \la{6--9} Used in sorting decending. That means that -1 will be at the top. If the page number is plus to begin with, then this is set to zero for sorting, so it will be at the bottom of the list. \la{10-29} This is the same as page number, unless the page number is negative, in which case this field is a roman numeral. \la{30--33\lbr34--333\lbr334--337\lbr338--637\lbr638--641\lbr642--941} The sort field. There are three arrays in this field. Each array consists of a 4 byte length, followed by the {\it field\_array} which is the length of the sort entry or subentry. It is these fields which will get sorted by @^Syncsort@> Syncsort. If there was a {\it sort\_as} used for a sort entry or subentry, that value passed in the {\it sort\_as} is placed here. \vs{24pt} \la{942--945\lbr946--1245\lbr1246--1249\lbr 1250--1549\lbr1550--1553\lbr1554--1853} The print field. The description is the same as the sort field. This is the way the index entry will print, but not necessarily how it will sort. \vs{96pt} @ @= @!max_pn_alpha=20; @!max_pnum=9; @ @= @!string_pass=string_type(max_lrecl); @!pass_pn_alpha=string_type(max_pn_alpha); @!pn_type=packed array[1..max_pnum] of char; @!pn_alpha_type=packed array[1..max_pn_alpha] of char; @!field_array = packed array[1..max_field] of char; @!field_type = packed record@| @!field_lngth :integer; @!field_level :field_array; end; @!sort_type = packed record@/ @!record_type :char; @!page_number :integer; @!abs_page_number :integer; @!page_string :pn_alpha_type; @!sort_part :packed array[1..3] of field_type; @!print :packed array[1..3] of field_type; end; @* Subroutines. For many of the following subroutines, the following global variable, {\it current\_level} is needed to indicate what index level we are presently processing. 0 = the primary index; 1 = subentry 1, and 2 = subentry 2. {\it on\_a\_roll} is `true' if we have a series of consecutive page numbers going. `false' otherwise. In other places, {\it print\_style}=1 for the paragraph style; it is 2 for the dash style; and it is 3 for the indent style. @= @!curr_level :integer; @!on_a_roll:boolean; @ The {\it write\_header} procedure will write out the primary and subentry level titles from {\it sort\_record.print}. @p procedure write_header(print_length:integer;print_field:field_array); var i:integer; begin case curr_level of 1: begin do_write(ix_file,'\goodbreak\hp '); @.\bs{}leavevmode@> @.\bs{}goodbreak@> @.\bs{}hp@> for i:=1 to print_length do do_write(ix_file,print_field[i]); case print_style of '1':begin do_write(ix_file,', '); end; '2','3':begin if id1_is_missing or dot_leadering then do_lnwrite(ix_file,' ') else do_lnwrite(ix_file,', '); end; othercases do_nothing endcases; end; 2: begin case print_style of '1':do_nothing; '2':begin do_write(ix_file,'\indentsubentry',ix,'1---'); @.\bs{}indentsubentry@> end; '3':begin do_write(ix_file,'\indentsubentry',ix,'1'); end; othercases do_nothing endcases; for i:=1 to print_length do do_write(ix_file,print_field[i]); if (print_style <> '1') or id2_is_missing or dot_leadering then do_lnwrite(ix_file,' ') else do_lnwrite(ix_file,', '); end; 3: begin case print_style of '1':do_nothing; '2':begin do_write(ix_file,'\indentsubentry',ix,'2---'); end; '3':begin do_write(ix_file,'\indentsubentry',ix,'2'); end; othercases do_nothing endcases; for i:=1 to print_length do do_write(ix_file,print_field[i]); if dot_leadering then do_lnwrite(ix_file,' ') else do_lnwrite(ix_file,', '); end; othercases do_nothing endcases end; @ The {\it numeric} function will take a packed array of length max\_pnum and convert that array (which is really the page number) into an integer. @p function numeric(simple_array:pn_type):integer; var @!i,j_mult,pn:integer; begin pn := 0; i := max_pnum;j_mult:=1; repeat if i = 1 then if simple_array[1] = '-' then pn := -1 * pn else pn := pn + ((ord(simple_array[1])-ord('0')) * j_mult) else pn := pn + ((ord(simple_array[i])-ord('0')) * j_mult); j_mult := j_mult * 10; decr(i); until i < 1; numeric:=pn; end; @ The {\it get\_numeric} function will take a packed array of length max\_pn\_alpha and convert that array (which is really the page number) into an integer. @p function get_numeric(x_string:string_type(max_pn_alpha)):integer; var @!i:integer; begin readstr(x_string,i); get_numeric:=i; end; @ The {\it strvalue} function takes an integer and converts it into a string. @^System dependent code@> @p procedure strvalue(x:integer; var results:pass_pn_alpha); var temp:string_type(max_pn_alpha); begin writestr(temp,x); results:=ltrim(temp); end; @ The {\it strconv} function takes an array and converts it into a string. @p function strconv(x:pn_alpha_type):string_type(max_pn_alpha); var i:integer; temp:string_type(max_pn_alpha); begin temp:='';i := 1; with sort_record do begin repeat if x[i] <> ' ' then temp := ccat(temp,str(x[i])); incr(i); until (i > max_pn_alpha) or (x[i] = ' '); end; strconv:=temp; end; @ The procedure {\it write\_print\_chars} will write the number of characters indicated in the parm field to the appropriate print field. There are three levels of print fields. Level 0 is the primary index, level 1 is the subentry 1 index, and level 2 is the subentry 2 index. After writing the number of characters to the print field, that number of characters is removed from the input record. You might notice that this procedure is very similar to the write\_sort\_chars procedure. The only difference in the two is that this procedure deals with how the index is to be printed after sorting. The former deals with how the index is to be sorted. Also, this procedure is called by {\it write\_sort\_chars}. @^System dependent code@> @p procedure write_print_chars(number_of_characters:integer); var m:integer; begin with sort_record.print[curr_level+1] do for m := 1 to number_of_characters do begin incr(field_lngth); field_level[field_lngth] := in_record[m]; end; remove_characters(number_of_characters); end; @ The procedure {\it write\_sort\_chars} will write the number of characters indicated in the parm field to the appropriate sort field. There are three levels of sort fields. Level 0 is the primary index, level 1 is the subentry 1 index, and level 2 is the subentry 2 index. After writing the characters to the sort fields, they will be removed from the input string. @^System dependent code@> @p procedure write_sort_chars(number_of_characters:integer); var m:integer; begin with sort_record.sort_part[curr_level+1] do for m := 1 to number_of_characters do begin incr(field_lngth); field_level[field_lngth] := in_record[m]; end; remove_characters(number_of_characters); end; @ This procedure processes subentries within the \bs{}index command. For example, the phrase ``Mt. St. Helens'' would be processed here for the entry created by the markup: \vs{12pt} {\lin{.5in} {\tt\bs{}index\{volcanos\bs{}subentry1\{Mt. St. Helens\}\}}\par} \vs{12pt} \nin Note that there are two levels of subentries, \bs{}subentry1 and \bs{}subentry2. You can use \bs{}sortas within a \bs{}subentry. But you cannot use \bs{}subentry2 within \bs{}subentry1 and vise versa. If you think it should be permitted, don't. Also, \bs{}blindentry is not permitted within \bs{}subentry. (I can't be flexible {\bf everywhere}.) @ First save the current level, then set the current level to the subentry level. Process until the subentry is completely digested. @^System dependent code@> @p procedure process_subentry(@!entry_level:integer); var temp_level :integer; digest :boolean; begin remove_characters(1);{Throw away the `1' or `2'}@/ temp_level := curr_level; curr_level := entry_level; digest := true; repeat @ until not digest; curr_level := temp_level; end; @ If the next character is a dollar sign, then we {\bf might} have a \bs{}sortas If the next character is a left curly brace, then check to see if we've reached the end of this subentry. If the character is not `\$' or `\{', then write the character to the print file. @= if in_record[1] = '$' then @ else if in_record[1] = '{' then @ else write_print_chars(1); @ If the next two characters are `\$\}' then we are are the end of the subentry. @= begin if (in_record[2] = '$') and (in_record[3] = '}') then begin digest := false; remove_characters(3); end else write_print_chars(1); end @ If the next three are `\{\$\}' then we {\it might} have a sortas. and check the next two characters to see if they are `{\tt sa}'. If so, we {\bf do} have a sortas. Otherwise write those 6 characters out. In any case, if we don't have a `\{\$\}' to begin with, then write the first character out (which was a `\$' if you remember from earlier.) @= if (in_record[2] = '{') and (in_record[3] = '$') and (in_record[4] = '}') then begin if (in_record[5] = 's') and (in_record[6] = 'a') then do_sort_as else write_print_chars(6); end else write_print_chars(1) @ The {\it equal\_arrays} function returns a true if the two arrays being passed are identical. False if not. @p function equal_arrays(fieldy:field_array;fieldz:field_array):boolean; var i :integer; still_checking :boolean; begin i := 1; still_checking := true; repeat if fieldy[i] <> fieldz[i] {if arrays not equal} then still_checking := false else incr(i); until (i>max_field) or not still_checking; equal_arrays := still_checking; end; @ The {\it plsort} procedure is written in PL/1 to call the CMS sort program Syncsort. This was necessary since you cannot call Syncsort from PASCAL/VS. The fields to be sorted are defined internally in the {\it plsort} program. This should be the same as the three sort fields in {\it sort\_type}. @:sort_type@> @:plsort@> @^PL/1@> @^Syncsort@> @p procedure plsort(var sort_rc:integer); fortran; @ This function converts to all uppercase. Notice that this is an EBCIDIC conversion, not an ASCII conversion to uppercase. @p function upper_case(x:char):char; var temp:char; begin if (ord(x)>=129)and(ord(x)<=169) then temp:=chr(ord(x)+64) else temp:=x; upper_case:=temp; end; @* Read Each Index Entry. Each line in the {\it ix\_file} is a separate index entry generated with the \bs{\it index markup}. This markup has the format \par{\tt\bs{}index\it n\tt\{\it entry\rm/\it markup\tt\}} \par \nin where ``{\it entry}'' is the textual material of the primary index. The ``{\it markup}'' may be one of the following submarkup which may {\bf only} appear within the \bs{\it index markup}. Never outside of it. \item{1.} {\tt\bs{}subentry{\it n\/}}: where {\it n} is ``1'' or ``2'', denoting the subentry level. You would use this submarkup in the following fashion: \par{\lin{+.5in}\tt\bs{}index1\{\rm entry \bs\rm subentry1\tt\{\it submarkup\tt\}\}\par} \indent where {\it submarkup} here can only be \bs{\it sortas}. \item{2.}{\tt\bs{}sortas}: This markup is used to indicate text for either the primary sort level, or one of the two subentry sort levels that is to be sorted, but not printed in the resulting index. For example, someone may want ``10 Downing Street'' to sort as ``ten Downing Street''. This would be accomplished by entering ``{\tt\bs{}index1\{10 Downing Street\bs{}sortas\{ten Downing Street\}\}}''. You can also use \bs{\it sortas} within the \bs{\it subentry} submarkup. \item{3.} {\tt\bs{}itpn}: This markup indicates this index number is to be printed in italics in the sorted index. To use it, enter: \par{\lin{+.5in}\tt\bs{}index1\{\rm entry \tt \bs{}itpn\}\par} \item{4.} {\tt\bs{}bdpn}: This markup indicates this index number is to be printed in bold in the sorted index. To use it, enter: \par{\lin{+.5in}\tt\bs{}index1\{\rm entry \tt \bs{}bdpn\}\par} \item{5.} {\tt\bs{}uspn}: This markup indicates this index number is to be underscored in the sorted index. To use it, enter: \par{\lin{+.5in}\tt\bs{}index1\{\rm entry \tt \bs{}uspn\}\par} \item{6.} {\tt\bs{}blindentry}: This markup indicates this index is to be a blind entry or ``cross reference''. It may appear inside of a standard entry or subentry. There is no page number associated with this submarkup. \par @ @= @!respect_case :boolean; @!dot_leadering :boolean; @!print_style:char; @!sort_record :sort_type; @!in_record :string_type(max_lrecl); @!temp_strvalue:pass_pn_alpha; @^System dependent code@> @ This procedure will read the {\it ix\_file} in until all index requests have been read. @p procedure read_all_entries; var @!i,j,k:integer; begin @ while not eof(ix_file) do begin @@/ @@/ @@/ @@/ @@/ @@/ @@/ end;end; @ The first index default variable passed is ``case=". A ``yes" value will respect the case in an index sort field. ``no" (the default) will convert everything to uppercase before sorting. The second index default ``style=" will be one of ``1", ``2'', or ``3'' depending on whether the style is ``paragraph'', ``dash'', or ``indented'' (which is the default). The third default read in here is the ``leadering=". A ``yes'' here will give dot leadering. Guess what ``no'' does. Dot leadering in the paragraph style ({\it print\_style}=1). If that has been requested, change {\it dot\_leadering} to false. @= readln(ix_file,in_record); if dosubstr(in_record,doindex(in_record,'=')+1) = 'no' then respect_case := false else respect_case := true; readln(ix_file,in_record);@/ print_style:=in_record[doindex(in_record,'=')+1];@/ readln(ix_file,in_record);@/ if dosubstr(in_record,doindex(in_record,'=')+1) = 'no' then dot_leadering := false else dot_leadering := true;@/ if dot_leadering and (print_style = '1') then begin writeln(messages, ' Error! Dot leadering not permitted with the paragraph style.'); writeln(messages,' Dot leadering will be disabled.'); dot_leadering := false; end; @ The {\it all\_blanks} variable is filled with all blanks. Other arrays of the same dimension of {\it all\_blanks} can be set to blank themselves by saying $array\leftarrow all\_blanks$. @= @!all_blanks:field_array; @ @= with sort_record do for i := 1 to max_levels do begin sort_part[i].field_level:= all_blanks; sort_part[i].field_lngth := 0; print[i].field_level:= all_blanks; print[i].field_lngth := 0; end; curr_level := 0; @ Read the next index request to be processed. Get rid of trailing blanks. @= readln(ix_file,in_record); in_record:=trim(in_record); @ The {\it sort\_type} is always in column 2. Column 1, 3 and 4 will always be curly braces. (`\{{\it s}\}\{' where ``{\it s}'' is the sort type.) After moving the {\it sort\_type} to the sort record, then delete the {\it sort\_type} and the three curly braces. @= sort_record.record_type := in_record[2]; remove_characters(4); @ The end of the page number is the next right curly brace (`\}'). Move all of the digits from the input record to the sort record, starting with the last digit and going forward. If a minus sign (`-') is in the page number (preface material), then put a minus sign in byte one of the sort record's page number. @= @!in_page_number:pn_type; @ Find and move the page number to the sort record. If it is negative, then its a roman numeral. In that case, move the page number to the {\it abs\_page\_number} to be sorted descending, as negative numbers should be. If its not negative, then move zero to {\it abs\_page\_number} so it will move to the bottom of that sort field. After it is moved, delete the appropriate number of characters to finish digesting the entry. @= k := doindex(in_record,'}'); {find the end of the page number} i := k - 1; for j := 1 to max_pnum do in_page_number[j] := '0'; j := max_pnum; repeat if in_record[i] = '-' then begin in_page_number[1] := '-'; i := 0; end else begin in_page_number[j] := in_record[i]; decr(i); decr(j); end; until i <= 0; remove_characters(k+1);@/ sort_record.page_number:=numeric(in_page_number);@/ if sort_record.page_number < 0 then sort_record.abs_page_number:=abs(sort_record.page_number) else sort_record.abs_page_number:=0; k := doindex(in_record,'}'); {find the end of the page number string} for i := 1 to max_pn_alpha do sort_record.page_string[i] := ' '; for i := 1 to k-1 do sort_record.page_string[i] := in_record[i]; remove_characters(k); @^System dependent code@> @ Process the rest of the input record. Each time you see a dollar sign, there could be a submarkup coming, so examine the next three characters. @= repeat if (in_record[1] = '$') and (length(in_record) >= 4) then @ else write_print_chars(1); until length(in_record) < 1; @ Once the index request has been digested, each sort field is checked to see if it is blank. If so, then {\it sort\_as} was not used, so copy the {\it print} field to the {\it sort} field. @= with sort_record do begin for i := 1 to max_levels do if sort_part[i].field_lngth=0 then begin sort_part[i].field_level := print[i].field_level; sort_part[i].field_lngth := print[i].field_lngth; check_case; end else check_case; end; @^System dependent code@> @ Ok, the record has been processed, so write it to the output file. @= sort_file@@:=sort_record; put(sort_file); @ Ok, a dollar sign signals the beginning of some sort of action code if the next three characters are `\{\$\}'. So lets look at them and see. @= begin if (in_record[2] = '{') and (in_record[3] = '$') and (in_record[4] = '}') then @ else write_print_chars(1);{Well then, the dollar sign must be part of the index} end @ If a {\it blindentry} (`{\tt be}') or {\it subentry} (`{\tt se}'), then perform the appropriate sections. @= begin if (in_record[5] = 'b') and (in_record[6] = 'e') then @ else if (in_record[5] = 's') and (in_record[6] = 'e') then @ else if (in_record[5] = 's') and (in_record[6] = 'a') then do_sort_as else write_print_chars(6);{Might as well write all 6 characters examined} end @ Everything up to the next `\{\$\}' is the blind entry. The page number for a blind entry will always be 999999999. @= begin remove_characters(6);{Throw away the `\$\{\$\}be'}@/ i := doindex(in_record,'{$}'); curr_level:=2; write_print_chars(i-1); curr_level:=0; remove_characters(3);{Throw away the `\{\$\}'} sort_record.page_number := 999999999; end @ Process the subentry, depending on whether or not it is {\it subentry}1 or {\it subentry}2. If not, ignore the whole thing. @= begin remove_characters(6); {Throw away the `\$\{\$\}se'} if in_record[1] = '1' then process_subentry(1) else if in_record[1] = '2' then process_subentry(2) else write_print_chars(1);{Perhaps someone entered \bs{}subentry3??} end @* Write Formated Index. This section of code takes the sorted index entries, merges them together, and writes them back out to the index file to be read in by \bs{}printindex in the user's program. This section consists of three procedures for accomplishing this feat: {\it add\_page\_number}, {\it digest\_the\_line}, and {\it read\_sorted\_records}. {\it numeric} is a function to convert the character string page number into an integer. @^System dependent code@> @ The procedure {\it add\_page\_number} is used to add the page number of the current sorted record to the {\it string\_bold} if a \bs{\it bdpn}, or to the {\it string\_underscore} if a \bs{\it uspn}, or otherwise to the {\it str\_build}. @p procedure add_page_number; var i:integer; @ @= @!prev_page_number:integer; @!prev_pg_string:string_type(max_pn_alpha); @!prev_record_type:char; @ First check to see if we have a new page number. If so, then add the page number to the string. Otherwise, only add the page number if the sort type is new and is not equal to ``6'' (italic). @p begin with sort_record do if prev_page_number = page_number then if (prev_record_type = record_type) and (record_type <> '9') then getout else if record_type = '6' then getout; @ end; @ {\it str\_build} is used to accumulate all of the page numbers. {\it str\_blind\_entry} is used to accumulate the blind entry information (typically there should be no more than one, but allowance is made for more.) @= @!str_build:string_type(max_lrecl); @!str_blind_entry:string_type(max_lrecl); {come on, who's going to have one that long?} @ This module will take the page number of the current sorted index record and add it to the build string, unless the record type is 9, which is a blind entry, in which case it is added to the blind entry string. Note that if the record type is 0 (bold), 4 (underscore) or 6 (italic), then the page number is prefaced with ``B'', ``U'' or ``I'' respectively. This identifies that the page number is to be emphasized when the build string is processed through the {\it digest\_the\_line} procedure. @= if sort_record.record_type = '9' then @ else begin @ end; prev_page_number := sort_record.page_number;@/ prev_pg_string := strconv(sort_record.page_string);@/ prev_record_type := sort_record.record_type; @ @= with sort_record.print[3] do begin if length(str_blind_entry) > 0 then add_cmma_blnk(str_blind_entry); for i := 1 to field_lngth do str_blind_entry := ccat(str_blind_entry,str(field_level[i])); end @ If the {\it page\_string[1]} is less than zero, its alphabetic and roman numeral processing is necessary. @= with sort_record do begin if ord(page_string[1]) < ord('0') then @ else @; end; @ @= begin if length(str_build) >= 1 then str_build:=ccat(',',str_build); strvalue(page_number,temp_strvalue); str_build := ccat(ccat(ccat(strconv(page_string), ','),temp_strvalue),str_build); if record_type = '0' then str_build := ccat('B',str_build) else if record_type = '4' then str_build := ccat('U',str_build) else if record_type = '6' then str_build := ccat('I',str_build) end @ @= begin if length(str_build) >= 1 then add_comma(str_build); if record_type = '0' then str_build := ccat(str_build,'B') else if record_type = '4' then str_build := ccat(str_build,'U') else if record_type = '6' then str_build := ccat(str_build,'I'); str_build := ccat(str_build,strconv(page_string)); end @ {\it output\_string} is used to collect all of the page numbers before writing them back out to the index file; {\it bold\_string} collects the bold page numbers; {\it us\_string} collects the underscored page numbers; {\it rm\_string} collects everything not bold or underscored. As you might expect, {\it italic\_last\_page} is true when the last page number of a `roll' is to be italic. @= @!output_string:string_type(max_lrecl); @!bold_string:string_type(max_lrecl); @!us_string:string_type(max_lrecl); @!rm_string:string_type(max_lrecl); @!italic_last_page:boolean; @^System dependent code@> @ {\it finish\_the\_process} is a procedure that is used to first check to see if we are {\it on\_a\_roll}, which means we are formatting something like pages 1-3. After doing that, if there were any bold page numbers, they are concatenated to the front of the {\it output\_string}; underscored page numbers (if any) are concatenated to the end of the {\it output\_string}. @p procedure finish_the_process; begin @;@/ @;@/ @; end; @ If there were any bold page numbers, then add them to the {\it output\_string}. @= if length(bold_string) > 0 then begin if length(output_string) > 0 then add_cmma_blnk(output_string); output_string := ccat(output_string,bold_string); end @ If we were on a roll (a consecutive series of page numbers) then finish the roll. Then if there were any roman or italic page numbers, add them to the {\it output\_string}. @= if on_a_roll then if italic_last_page then rm_string := ccat(ccat(ccat(rm_string,'{\it '),trim(prev_pg_string)),'}') else rm_string := ccat(rm_string,prev_pg_string); @.\bs{}it@> if length(rm_string) > 0 then begin if length(output_string) > 0 then add_cmma_blnk(output_string); output_string := ccat(output_string,rm_string); end @ If there were any underscored page numbers, add them to the {\it output\_string} here. @= if length(us_string) > 0 then begin if length(output_string) > 0 then add_cmma_blnk(output_string); output_string := ccat(output_string,us_string); end @ The procedure {\it start\_digesting} will initialize a few variables, then add the {\it current\_page} to the appropriate list. @p procedure start_digesting; begin @;@/ prev_pg_string := curr_str_page;@/ @; end; @ @= italic_last_page := false; bold_string := ''; us_string :=''; rm_string:=''; starting_to_process:=false; on_a_roll:=false @ @= print_type:= curr_str_page[1]; if (print_type='B') or (print_type='I') or (print_type='U') then curr_str_page:=dosubstr(curr_str_page,2) else print_type := ' ' @ @= if print_type = 'B' then do_when_bold else if print_type = 'I' then do_when_italic else if print_type = 'U' then do_when_underscore else do_when_roman @^System dependent code@> @ The procedure {\it digest\_the\_line} will take the full {\it str\_build} and add the dashes when there is a run of page numbers (i.e. ``{\tt 1,2,3}'' bec ``1-3''); add the bold page numbers in front of the string (i.e. ``{\tt 1,2,B3,3,4}'' prints as ``{\bf 3},1-4''. Note that page 3 had to appear twice ``{\tt B3,3}''. If it had only appeard as a bold page number and not the default, ``{\tt 1,2,B3,4}'', you would get ``1-2,{\bf 3},4''); add the underscored page numbers to the end of the string (similar to bold page numbers, ``1,2,U3,3,4'' prints as ``1-4,\us{3}''); and print in italics when necessary. @p procedure digest_the_line; var i:integer; @!temp_roman:string_type(max_pn_alpha); @!no_page_numbers:boolean; begin @;@/ @; end; @ @= @!starting_to_process :boolean; @!print_type:char; @!curr_str_page :string_type(max_pn_alpha); @!curr_num_page :integer; @ Initialize the variables, then read each page number until the whole string is digested, then finish off the page number string before returning. If {\it str\_build} is null, then we are doing a blind entry with no page numbers associated. In that case, initialize the highlighting strings. @= output_string := ''; starting_to_process := true; no_page_numbers:=false; if length(str_build) > 0 then repeat @ until length(str_build) < 1 else if starting_to_process then begin @; no_page_numbers:=true; end; finish_the_process; @ First get the next page number. Then if we are starting, initialize everything. If we have already started, then check to see if we have a consecutive page number sequence (i.e. pages 1,2,3,4 etc.) which is called a `roll'. @= @;@/ if starting_to_process then start_digesting else @; prev_page_number:=curr_num_page; @ @= i := doindex(str_build,','); if i < 1 then begin curr_str_page:=str_build; str_build:='';end else begin curr_str_page:=dosubstr(str_build,1,i-1); str_build := dosubstr(str_build,i+1); end; @;@/ if ord(curr_str_page[1]) < ord('0') {Then its alphabetic} then @ else curr_num_page:=get_numeric(curr_str_page); @ @= begin i := doindex(str_build,','); if i < 1 then begin temp_roman:=str_build; str_build:='';end else begin temp_roman:=dosubstr(str_build,1,i-1); str_build := dosubstr(str_build,i+1); end; curr_num_page:=get_numeric(temp_roman); end @ If we are on a roll, then the previous page number will be one less than the current page number. If this is not the case, then {\it finish\_the\_process} and {\it start\_digesting} all over again. @= if ((prev_page_number = curr_num_page) and (print_type <> ' ')) or ((prev_page_number>=0) and (prev_page_number = curr_num_page - 1)) or ((prev_page_number<0) and (prev_page_number = curr_num_page + 1)) then begin @ prev_pg_string:=curr_str_page; end else begin finish_the_process; start_digesting; end @ Ok, so we're on a roll, right? If the {\it print\_type} is ``B'' (bold) or ``U'' (underscored) then do those and return. Otherwise, check to se if we were previously {\it on\_a\_roll}. If we weren't, then add the hyphen to the starting page number. In either case, move the {\it current\_page} to the {\it prev\_pg\_string}. Lastly, if the page number is italic, then set {\it italic\_last\_page} to true, in case it ends up being the last page in the string. @= if print_type = 'B' then do_when_bold else if print_type = 'U' then do_when_underscore else begin if not on_a_roll then begin on_a_roll := true; rm_string:=ccat(rm_string,'--'); end; if print_type = 'I' then italic_last_page := true else italic_last_page := false; end; @ Before the page numbers are actually written, we need to write out the ID depending on which level we are formatting. Level 1 is the primary level, level 2 is the subentry1, and level 3 is subentry2. Then, we need to do a couple of things depending on whether there is dot leadering going on or not. Finally, write the output string to the formatted index file. @= write_prev_header; if dot_leadering then begin @ end else begin @ end; do_lnwrite(ix_file,output_string); @ If there is dot leadering, then add the blind entry first, before adding the leadering. @= if length(str_blind_entry) > 0 then begin if length(output_string) > 0 then add_cmma_blnk(output_string); output_string := ccat(output_string,str_blind_entry); end; output_string := ccat(ccat('\leader{}',output_string),'\par'); @.\bs{}leader@> @.\bs{}par@> @ @= @!new_level_1:boolean; @ If there is not dot leadering, then the blind entry goes at the end of the page, except for the paragraph style ({\it print\_style}=1). Also, if the paragraph style, then do not add the {\tt \bs{}par} at the end. Instead, add a comma if there is a new level 1 entry. @= if (print_style <> '1') and (length(str_blind_entry) > 0)@/ then begin if length(output_string) > 0 then add_cmma_blnk(output_string); output_string := ccat(output_string,str_blind_entry); end; if print_style = '1' then if new_level_1 and ((length(str_blind_entry)=0) or no_page_numbers) then do_nothing else add_cmma_blnk(output_string) else output_string := ccat(output_string,'\par'); @.\bs{}par@> @ The {\it build\_sorted\_index} procedure is the driving force behind formatting the index. The basic process is to read all of the sorted records in and create a record called {\it str\_build}. There will be one of these for each primary and subentry level index. After doing the {\it str\_build}, this record is then read through again to add the {\it\TeX T1} markup necessary to print out the sorted index line. @p procedure build_sorted_index; var @!first_time_through:boolean; @ @= @!prev_sr1:field_array; @!prev_sr2:field_array; @!prev_sr3:field_array; @!prev_pn1:field_array; @!prev_ln1:integer;{the length of pn1} @!prev_pn2:field_array; @!prev_ln2:integer;{the length of pn2} @!prev_pn3:field_array; @!prev_ln3:integer;{the length of pn3} @!id1_is_missing:boolean; @!id2_is_missing:boolean; @ The first thing we need to do is read the next sorted record. If this is the first time ever through this procedure, then do some quick initializing. Then, once we come to a new ID, we should write out the previous index page numbers, etc. If the ID has not changed from the previous record, then add the page number given here on the sorted record to the {\it str\_build}. @p begin first_time_through:=true; while not eof(sort_file) do begin sort_record:=sort_file@@; get(sort_file); if first_time_through then begin @ end; if not equal_arrays(prev_sr1,sort_record.sort_part[1].field_level) then begin @ end else if not equal_arrays(prev_sr2,sort_record.sort_part[2].field_level) then begin @ end else if not equal_arrays(prev_sr3,sort_record.sort_part[3].field_level) and (sort_record.record_type <> '9') then begin @ end else add_page_number; end; @ end; @ First write out the {\it \bs{}everyindex} record at the beginning of the index file. @= do_lnwrite(ix_file,'\everyindex{',ix,'}'); @.\bs{}everyindex@> @ These initializations only get done at the beginning of the first sort record. @= first_time_through:=false; id1_is_missing:=false; id2_is_missing:=false; new_level_1:=false; with sort_record do begin prev_sr1:=sort_part[1].field_level; prev_sr2:=sort_part[2].field_level; prev_sr3:=sort_part[3].field_level; prev_pn1:=print[1].field_level; prev_ln1:=print[1].field_lngth; prev_pn2:=print[2].field_level; prev_ln2:=print[2].field_lngth; prev_pn3:=print[3].field_level; prev_ln3:=print[3].field_lngth; end; prev_page_number:=0; str_blind_entry:='';@/ str_build:='';@/ @ If this is not a blind entry, then check to see if sort records for the first level id are missing. If they are, then the id name itself will be printed in {\it id1\_missing\_check}. @= if sort_record.record_type <> '9' then id1_missing_check; @ Only alpha (a--z and A--Z) and numeric (0--9) characters get an {\it \bs{}everyletterbreak}. Punctuation, for example, will not get an {\it \bs{}everyletterbreak}. @= with sort_record.sort_part[1] do begin if ((field_level[1]>='a') and (field_level[1]<='i')) or @/ ((field_level[1]>='j') and (field_level[1]<='r')) or @/ ((field_level[1]>='s') and (field_level[1]<='z')) or @/ ((field_level[1]>='A') and (field_level[1]<='I')) or @/ ((field_level[1]>='J') and (field_level[1]<='R')) or @/ ((field_level[1]>='S') and (field_level[1]<='Z')) or @/ ((field_level[1]>='0') and (field_level[1]<='9')) then do_lnwrite(ix_file,'\everyletterbreak{',ix,'}{',field_level[1],'}') else do_lnwrite(ix_file,'\everyletterbreak{',ix,'}{}'); end @.\bs{}everyletterbreak@> @ First we need to {\it digest\_the\_line} which is the current index's entry. {\it digest\_the\_line} will write out the formatted index unless we are in the paragraph style of formatted index ({\it print\_style}=1). If we are in the paragraph style, then we need to write out any {\it str\_blind\_entry} pending. @= new_level_1:=true; digest_the_line; if print_style = '1' then do_lnwrite(ix_file,str_blind_entry,'\par'); new_level_1:=false; @.\bs{}par@> @ We have just found the start of a new primary index level. First we need to process the previous index's entry. If there is a blind entry, and it is style `1', then add it now. Next, if we are at a letter break (i.e., going from the sorted ``A'' primary index letters to the ``B''s) then write the ``{\tt \bs{}everyletterbreak}'' to the formatted index file. @= @ with sort_record.sort_part[1] do begin if (field_level[1] <> prev_sr1[1]) then if (((field_level[1]>='a') and (field_level[1]<='i')) or @/ ((field_level[1]>='j') and (field_level[1]<='r')) or @/ ((field_level[1]>='s') and (field_level[1]<='z')) or @/ ((field_level[1]>='A') and (field_level[1]<='I')) or @/ ((field_level[1]>='J') and (field_level[1]<='R')) or @/ ((field_level[1]>='S') and (field_level[1]<='Z')) or @/ ((field_level[1]>='0') and (field_level[1]<='9'))) @/ then do_lnwrite(ix_file,'\everyletterbreak{',ix,'}{',field_level[1],'}') else do_lnwrite(ix_file,'\everyletterbreak{',ix,'}{}'); end; @.\bs{}everyletterbreak@> @ The next thing to be done when starting a new primary index is to check the subentries. If they exist, then you can assume that this primary level index has no page numbers associated with it. The macro {\it id1\_missing\_check} accomplishes this. @= curr_level:=1;@/ if sort_record.record_type <> '9' then id1_missing_check; @ The complicated stuff is done. So now we can actually do some simple initializations to get this new level 1 (primary level) sort rolling. @= str_build:=''; str_blind_entry:=''; with sort_record do begin prev_sr1:=sort_part[1].field_level; prev_sr2:=sort_part[2].field_level; prev_sr3:=sort_part[3].field_level; prev_pn1:=print[1].field_level; prev_ln1:=print[1].field_lngth; prev_pn2:=print[2].field_level; prev_ln2:=print[2].field_lngth; prev_pn3:=print[3].field_level; prev_ln3:=print[3].field_lngth; prev_page_number:=0; end; add_page_number; @ We have just found the start of a new subentry1 index level. These modules are similar to {\it Start a New Level 1}, but not as detailed since it is dealing with fewer levels. First we need to {\it digest\_the\_line} which is the previous index's entry. {\it digest\_the\_line} will write out the formatted index unless we are in the paragraph style of formatted index ({\it print\_style}=1). @= digest_the_line; @ The next thing to be done when starting a new subentry1 index is to check the subentry2. If it exists, then you can assume that this subentry index has no page numbers associated with it. This is done in {\it id2\_missing\_check}. @= id2_missing_check; @ The complicated stuff is done. So now we can actually do some simple initializations to get this new level 2 (subentry2 level) sort rolling. @= str_build:=''; if print_style <> '1' then str_blind_entry:=''; with sort_record do begin prev_sr2:=sort_part[2].field_level; prev_sr3:=sort_part[3].field_level; prev_pn2:=print[2].field_level; prev_ln2:=print[2].field_lngth; prev_pn3:=print[3].field_level; prev_ln3:=print[3].field_lngth; prev_page_number:=0; end; add_page_number; @ We have just found the start of a new subentry2 index level. These modules are similar to {\it Start a New Level 2}, but not as detailed since it is dealing with the lowest level. First we need to {\it digest\_the\_line} which is the previous index's entry. {\it digest\_the\_line} will write out the formatted index unless we are in the paragraph style of formatted index ({\it print\_style}=1). @= digest_the_line; curr_level:=3; @ Do some initializations to get this new level 3 (subentry2) sort rolling. @= str_build:=''; if print_style <> '1' then str_blind_entry:=''; with sort_record do begin prev_sr3:=sort_part[3].field_level; prev_pn3:=print[3].field_level; prev_ln3:=print[3].field_lngth; prev_page_number:=0; end; add_page_number; @* Main Program. Ok, here is the main program. First we initialize (all\_blanks); then set the ix\_file for input and the sort\_file for output; read all of the entries, processing each one; close the files; sort; read the sorted file in and build the entries, writing them back to the ix\_file to be read in by the index markup. WHEW!! @p begin @@/ termout(messages); reset_file(ix);@/ file_rewrite(s_file_number); {Should always be file 9}@/ read_all_entries;@/ close(ix_file);@/ close(sort_file);@/ @@/ reset_file(s_file_number);@/ file_rewrite(ix);@/ build_sorted_index;@/ end; @ @= for i := 1 to max_field do all_blanks[i] := ' '; @ As mentioned earlier, {\it plsort} is an external PL/1 subroutine @^PL/1@> @^Syncsort@> which is used to call Syncsort to sort the file. The sort fields are as follows: \halign{\hskip3em\hfill#\hfill&&\hskip3em\hfill#\hfill\cr \bf Starting Column&\bf Length&\bf Field Description&\bf Order\cr 38&300&character&ascending\cr 342&300&character&ascending\cr 646&300&character&ascending\cr 10&4&binary&descending\cr 6&4&binary&ascending\cr} @= sort_rc:integer; @ @= plsort(sort_rc); if sort_rc = 0 then writeln(messages,'Index Successfully Completed') else writeln(messages,'Index Failed'); @* Index. All modules in which an identifier is used are listed with that identifier, except that reserved words are indexed only when they appear in format definitions, and the appearances of identifiers in module names are not indexed. Underlined entries correspond to where the identifier was declared. Error messages, control sequences put into the output, and a few other things like ``Syncsort'' are indexed here too.