%% This is part of OpTeX project, see http://petr.olsak.net/optex \_codedecl \makeindex {Makeindex and sorting <2020-04-26>} % loaded in format \_doc ----------------------------- \^`\makeindex` implements sorting algorithm at \TeX/ macro-language level. You need not any external program. There are two passes in sorting algorithm. Primary pass does not distinguish between a group o letters (typically non-accented and accented). If the result of comparing two string is equal in primary pass then secondary pass is started. It distinguish between variously accented letters. Czech rules, for example says: not accented before dieresis before acute before circumflex before ring. At less priority: lowercase letters must be before uppercase letters. The \`\_sortingdata``` implements these rules for the language . The groups between commas are not distinguished in the first pass. The second pass distinguishes all characters mentioned in the `\_sortingdata` (commas are ignored). The order of letters in the `\_sortingdata` macro is significant for sorting algorithm. The Czech rules (`cs`) are implemented here: \_cod ----------------------------- \_def \_sortingdatacs {% /,{ },-,&,@,% aAäÄáÁ,% bB,% cC,% čČ,% dDďĎ,% eEéÉěĚ,% fF,% gG,% hH,% ^^T^^U^^V,% ch Ch CH iIíÍ,% jJ,% kK,% lLĺĹľĽ,% mM,% nNňŇ,% oOöÖóÓôÔ,% pP,% qQ,% rRŕŔ,% řŘ,% sS,% šŠ,% tTťŤ,% uUüÜúÚůŮ,% vV,% wW,% xX,% yYýÝ,% zZ,% žŽ,% 0,1,2,3,4,5,6,7,8,9,'% } \_doc ----------------------------- Characters ignored by sorting algorithm are declared in \`\_ignoredchars```. The compound characters (two or more characters interpreted as one character in sorting algorithm) is mapped to single invisible characters in \`\_compoundchars```. Czech rules declares ch or Ch or CH as a single letter sorted between H and I. See \`\_sortingdatacs` above where these declared characters are used. The characters declared in `\_ignoredchars` are ignored in first pass without additional condition. All characters are taken into account in second pass: ASCII characters with code $\lq65$ are sorted first if they are not mentioned in the `\_sortingdata` macro. Others not mentioned characters have undefined behavior during sorting. \_cod ----------------------------- \_def \_ignoredcharscs {.,;?!:'"|()[]<>=+} \_def \_compoundcharscs {ch:^^T Ch:^^U CH:^^V} % DZ etc. are sorted normally \_doc ----------------------------- Slovak sorting rules are the same as Czech. The macro `\_sortingdatacs` includes Slovak letters too. Compound characters are the same. English sorting rules can be defined by `\_sortingdatacs` too because English alphabet is subset of Czech and Slovak alphabets. Only difference: \`\_compoundcharsen` is empty in English rules. You can declare these macros for more languages, if you wish to use `\makeindex` with sorting rules in respect to your language. Note: if you need to map compound characters to a character, don't use `^^I` or `^^M` because these characters have very specific category code. And use space to separate more mappings, like in \`\_compoundcharscs` above. \_cod ----------------------------- \_let \_sortingdatask = \_sortingdatacs \_let \_compoundcharssk = \_compoundcharscs \_let \_ignoredcharssk = \_ignoredcharscs \_let \_sortingdataen = \_sortingdatacs \_def \_compoundcharsen {} \_let \_ignoredcharsen = \_ignoredcharscs \_doc ----------------------------- Preparing to primary pass is implemented by the \`\_setprimarysorting` macro. It is called from `\makeindex` macro and all processing of sorting is in a group. \_cod ----------------------------- \_def\_setprimarysorting {% \_ea\_let \_ea\_sortingdata \_csname _sortingdata\_sortinglang\_endcsname \_ea\_let \_ea\_compoundchars \_csname _compoundchars\_sortinglang\_endcsname \_ea\_let \_ea\_ignoredchars \_csname _ignoredchars\_sortinglang\_endcsname \_ifx \_sortingdata\_relax \_addto\_nold{ sortingdata}% \_let \_sortingdata = \_sortingdataen \_fi \_ifx \_compoundchars\_relax \_addto\_nold{ compoundchars}% \_let \_compoundchars = \_compoundcharsen \_fi \_ifx \_ignoredchars\_relax \_addto\_nold{ ignoredchars}% \_let \_ignoredchars = \_ignoredcharsen \_fi \_ifx \_compoundchars\_empty \_else \_edef \_compoundchars {\_detokenize\_ea{\_compoundchars} }\_fi % all must be catcode 12 \_def \_act ##1{\_ifx##1\_relax \_else \_ifx##1,\_advance\_tmpnum by1 \_else \_lccode`##1=\_tmpnum \_fi \_ea\_act \_fi}% \_tmpnum=65 \_ea\_act \_sortingdata \_relax \_def \_act ##1{\_ifx##1\_relax \_else \_lccode`##1=`\^^I \_ea\_act \_fi}% \_ea\_act \_ignoredchars \_relax } \_doc ----------------------------- Preparing to secondary pass is implemented by the \`\_setsecondarysorting` macro. \_cod ----------------------------- \_def\_setsecondarysorting {% \_def \_act ##1{\_ifx##1\_relax \_else \_ifx##1,\_else \_advance\_tmpnum by1 \_lccode`##1=\_tmpnum \_fi \_ea\_act \_fi}% \_tmpnum=65 \_ea\_act \_sortingdata \_relax } \_doc ----------------------------- Strings to be sorted are prepared in `\,` control sequences (in order to save `\TeX` memory). The \`\_preparesorting` `\,` converts `` to `\_tmpb` with respect to the data initialized in \^`\_setprimarysorting` or \^`\_setsecondarysorting`.\nl The compoud characters are converted to single characters by the \`\_docompound` macro. \_cod ----------------------------- \_def \_preparesorting #1{% \_edef \_tmpb {\_ea\_ignorefirst\_csstring #1}% \, -> \_ea \_docompound \_compoundchars \_relax:{} % replace compound characters \_lowercase \_ea{\_ea\_def \_ea\_tmpb \_ea{\_tmpb}}% convert in respect to \_sortingdata \_ea\_replstring \_ea\_tmpb \_ea{\_csstring\^^I}{}% remove ignored characters } \_def \_docompound #1:#2 {% \_ifx\_relax#1\_else \_replstring\_tmpb {#1}{#2}\_ea\_docompound \_fi } \_def \_ignorefirst#1{} \_doc ----------------------------- Macro \`\_isAleB` `\, \,` returns the result of comparison of given two strings to \`\_ifAleB` control sequence. Usage: `\isAleB \, \, \_ifAleB ... \_else ... \_fi` The converted strings (in respect of the data prepared for first pass) must be saved as values of `\,` and `\,` macros. The reason is speed: we don't want to convert them repeatedly in each comparison. \nl The macro \`\_testAleB` `&\_relax\_relax \,\,`\nl does the real work. It reads first character from both converted strings, compares them and if it is equal then calls itself recursively else gives result. \_cod ----------------------------- \_newifi \_ifAleB \_def\_isAleB #1#2{% \_edef\_tmpb {#1&\_relax#2&\_relax}% \_ea \_testAleB \_tmpb #1#2% } \_def\_testAleB #1#2\_relax #3#4\_relax #5#6{% \_if #1#3\_if #1&\_testAleBsecondary #5#6% goto to the second pass:: \_else \_testAleB #2\_relax #4\_relax #5#6% \_fi \_else \_ifnum `#1<`#3 \_AleBtrue \_else \_AleBfalse \_fi \_fi } \_def\_testAleBsecondary#1#2{% \_bgroup \_setsecondarysorting \_preparesorting#1\_let\_tmpa=\_tmpb \_preparesorting#2% \_edef\_tmpb{\_tmpa0\_relax\_tmpb1\_relax}% \_ea\_testAleBsecondaryX \_tmpb \_egroup } \_def\_testAleBsecondaryX #1#2\_relax #3#4\_relax {% \_if #1#3\_testAleBsecondaryX #2\_relax #4\_relax \_else \_ifnum `#1<`#3 \_global\_AleBtrue \_else \_global \_AleBfalse \_fi \_fi } \_doc ----------------------------- Merge sort is very effectively implemented by \TeX/ macros. The following code is created by my son Miroslav. The \`\_mergesort` macro expects that all items in `\_iilist` are separated by comma when it starts. It ends with sorted items in `\_iilist` without commas. So `\_dosorting` macro must prepare commas between items. \_cod ----------------------------- \_def\_mergesort #1#2,#3{% by Miroslav Olsak \_ifx,#1% % prazdna-skupina,neco, (#2=neco #3=pokracovani) \_addto\_iilist{#2,}% % dvojice skupin vyresena \_sortreturn{\_fif\_mergesort#3}% % \mergesort pokracovani \_fi \_ifx,#3% % neco,prazna-skupina, (#1#2=neco #3=,) \_addto\_iilist{#1#2,}% % dvojice skupin vyresena \_sortreturn{\_fif\_mergesort}% % \mergesort dalsi \_fi \_ifx\_end#3% % neco,konec (#1#2=neco) \_ifx\_empty\_iilist % neco=kompletni setrideny seznam \_def\_iilist{#1#2}% \_sortreturn{\_fif\_fif\_gobbletoend}% % koncim \_else % neco=posledni skupina nebo \end \_sortreturn{\_fif\_fif % spojim \indexbuffer+necoa cele znova \_edef\_iilist{\_ea}\_ea\_mergesort\_iilist#1#2,#3}% \_fi\_fi % zatriduji: p1+neco1,p2+neco2, (#1#2=p1+neco1 #3=p2) \_isAleB #1#3\_ifAleB % p1p2 \_addto\_iilist{#3}% % p2 do bufferu \_sortreturn{\_fif\_mergesort#1#2,}% % \mergesort p1+neco1,neco2, \_fi \_relax % zarazka, na ktere se zastavi \sortreturn } \_def\_sortreturn#1#2\_fi\_relax{#1} \_def\_fif{\_fi} \_def\_gobbletoend #1\_end{} \_doc ----------------------------- The \`\_dosorting` `\list` macro redefines `\list` as sorted `\list`. The `\list` have to include control sequences in the form `\`. These control sequences will be sorted in respect to without change of meanings of these control sequences. Their meanings are irrelevant when sorting. The first character in `\` should be whatever. It does not influence the sorting. \OpTeX/ uses comma at this place for sorting indexes: `\, \, \, ...`. The actual language (chosen for hyphenation patterns) is used for sorting data. If the `\_sortinglang` macro is defined as ``(for example `\def\sortinglang{de}`) then this has precedence and actual language is not used. Moreover, if you specify \`\_asciisortingtrue` then ASCII sorting will be processed and all language sorting data will be ignored. \_cod ----------------------------- \_newifi \_ifasciisorting \_asciisortingfalse \_def\_dosorting #1{% \_begingroup \_def\_nold{}% \_ifx\_sotringlang\_undefined \_edef\_sortinglang{\_cs{_lan:\_the\_language}}\_fi \_ifasciisorting \_edef\_sortinglang{ASCII}% \_def \_preparesorting##1{\_edef\_tmpb{\_ea\_ignorefirst\_csstring##1}}% \_let \_setsecondarysorting=\_relax \_else \_setprimarysorting \_fi \_message{OpTeX: Sorting \_string#1 (\_sortinglang) ...^^J}% \_ifx\_nold\_empty\_else \_opwarning{Missing\_nold\_space for language (\_sortinglang)}\_fi \_def \_act##1{\_preparesorting ##1\_edef##1{\_tmpb}}% \_ea\_xargs \_ea\_act #1;% \_def \_act##1{\_addto #1{##1,}}% \_edef #1{\_ea}\_ea\_xargs \_ea\_act #1;% \_edef \_iilist{\_ea}\_ea\_mergesort #1\_end,\_end \_ea\_endgroup \_ea\_def\_ea#1\_ea{\_iilist}% } \_doc ----------------------------- The \`\makeindex` prints the index. First, it sorts the `\_iilist` second, it prints the sorted `\_iilist`, each item is printed using \^`\_printindexitem`. \_cod ----------------------------- \_def\_makeindex{\_par \_ifx\_iilist\_empty \_opwarning{index data-buffer is empty. TeX me again}% \_incr\_unresolvedrefs \_else \_dosorting \_iilist % sorting \_iilist \_bgroup \_rightskip=0pt plus1fil \_exhyphenpenalty=10000 \_leftskip=\_iindent \_ea\_xargs \_ea\_printindexitem \_iilist ;\_par \_egroup \_fi } \_public \makeindex ; \_doc ----------------------------- The \`\_printindexitem` `\,` prints one item to the index. If `\_,` is defined then this is used instead real (this exception is declared by `\iis` macro). Else is printed by \^`\_printii`. Finally, \^`\_printiipages` prints the value of `\,`, i.e. the list of pages. \_cod ----------------------------- \_def\_printindexitem #1{% \_ifcsname _\_csstring #1\_endcsname \_ea\_ea\_ea \_printii \_csname _\_csstring #1\_endcsname &% \_else \_ea\_ea\_ea\_printii \_ea\_ignorefirst \_csstring #1&% \_fi \_ea\_printiipages #1& } \_doc ----------------------------- \`\_printii` `&` does more intelligent work because we are working with words in the form `//`. The \^`\everyii` tokens register is applied before `\noindent`. User can declare something special here. The \`\_newiiletter``{}` macro is empty by default. It is invoked if first letter of index entries is changed. You can declare a design between index entries here. You can try, for example: \begtt \def\_newiiletter#1#2{% \bigskip \hbox{\setfontsize{at15pt}\bf\uppercase{#1}}\medskip} \endtt \_cod ----------------------------- \_def\_printii #1#2&{% \_ismacro\_lastii{#1}\_iffalse \_newiiletter{#1}{#2}\_def\_lastii{#1}\_fi \_gdef\_currii{#1#2}\_the\_everyii\_noindent \_hskip-\_iindent \_ignorespaces\_printiiA#1#2//} \_def\_printiiA #1/{\_if^#1^\_let\_previi=\_currii \_else \_ea\_scanprevii\_previi/&\_edef\_tmpb{\_detokenize{#1}}% \_ifx\_tmpa\_tmpb \_iiemdash \_else#1 \_gdef\_previi{}\_fi \_expandafter\_printiiA\_fi } \_def\_iiemdash{\_kern.1em---\_space} \_def\_lastii{} \_def\_newiiletter#1#2{} \_def\_scanprevii#1/#2&{\_def\_previi{#2}\_edef\_tmpa{\_detokenize{#1}}} \_def\_previi{} % previous index item \_doc ----------------------------- \`\_printiipages` `&` gets `` in the form `:,:,...:` and it converts them to `, , --, ` etc. The same pages must be printed only once and continuous consequences of pages must be compressed to the form -. Moreover, the consequence is continuous only if all pages have the same . Empty is most common, pages with `b` must be printed as bold and with `i` `` as italics. Moreover, the `` mentioned here are , but we have to print . The following macros solves these tasks. \_cod ----------------------------- \_def\_printiipages#1&{\_let\_pgtype=\_undefined \_tmpnum=0 \_printpages #1,:,\_par} \_def\_printpages#1:#2,{% state automaton for compriming pages \_ifx,#1,\_uselastpgnum \_else \_def\_tmpa{#2}% \_ifx\_pgtype\_tmpa \_else \_let\_pgtype=\_tmpa \_uselastpgnum \_usepgcomma \_pgprint#1:{#2}% \_tmpnum=#1 \_returnfi \_fi \_ifnum\_tmpnum=#1 \_returnfi \_fi \_advance\_tmpnum by1 \_ifnum\_tmpnum=#1 \_ifx\_lastpgnum\_undefined \_usepgdash\_fi \_edef\_lastpgnum{\_the\_tmpnum:{\_pgtype}}% \_returnfi \_fi \_uselastpgnum \_usepgcomma \_pgprint#1:{#2}% \_tmpnum=#1 \_relax \_ea\_printpages \_fi } \_def\_returnfi #1\_relax{\_fi} \_def\_uselastpgnum{\_ifx\_lastpgnum\_undefined \_else \_ea\_pgprint\_lastpgnum \_let\_lastpgnum=\_undefined \_fi } \_def\_usepgcomma{\_ifnum\_tmpnum>0, \_fi} % comma+space between page numbers \_def\_usepgdash{\_hbox{--}} % dash in the -- form \_doc ----------------------------- You can re-define \`\_pgprint` `:{}` if you need to implement more . \_cod ----------------------------- \_def\_pgprint #1:#2{% \_ifx ,#2,\_pgprintA{#1}\_returnfi \_fi \_ifx b#2{\_bf \_pgprintA{#1}}\_returnfi \_fi \_ifx i#2{\_it \_pgprintA{#1}}\_returnfi \_fi \_ifx u#2\_pgu{\_pgprintA{#1}}\_returnfi \_fi \_pgprintA{#1}\_relax } \_def\_pgprintA #1{\_ilink[pg:#1]{\_cs{_pgi:#1}}} % \ilink[pg:]{} \_def\_pgu#1{\_leavevmode\_vtop{\_hbox{#1}\kern.3ex\_hrule}} \_doc ----------------------------- The \`\iindex``{}` puts one to the index. It writes \^`\_Xindex``{}{}` to the `.ref` file. All othes variants of indexing macros expands internally to `\iindex`. \_cod ----------------------------- \_def\_iindex#1{\_isempty{#1}\_iffalse\_openref{\def~{ }% \edef\_act{\_noexpand\_wref\_noexpand\_Xindex{{#1}{\_iitypesaved}}}\_act}\_fi} \_public \iindex ; \_doc ----------------------------- The \`\_Xindex``{}{}` stores `\,` to the `\_iilist` if there is first occurrence of the . The list of pages where `` occurs, is the value of the macro `\,`, so the `:` is appended to this list. Moreover, we need a mapping from to ``, because we print `` in the index, but hyperlinks are implemented by ``. So, the macro `\_pgi:` is defined as ``. \_cod ----------------------------- \_def \_iilist {} \_def \_Xindex #1#2{\_ea\_XindexA \_csname ,#1\_ea\_endcsname \_currpage {#2}} \_def \_XindexA #1#2#3#4{% #1=\, #2= #3= #4= \_ifx#1\_relax \_global\_addto \_iilist {#1}% \_gdef #1{#2:#4}% \_else \_global\_addto #1{,#2:#4}% \_fi \_sxdef{_pgi:#2}{#3}% } \_doc ----------------------------- The implementation of macros \`\ii`, \`\iid`, \`\iis` follows. Note that `\ii` works in horizontal mode on order to the `\write` whatsit is not broken from the following word. If you need to keep vertical mode, use \^`\iindex``{}` directly. \nl The \`\iitype` `{}` saves the `` to the \`\_iitypesaved` macro. It is used in the \^`\iindex` macro. \_cod ----------------------------- \_def\_ii #1 {\_leavevmode\_def\_tmp{#1}\_iiA #1,,\_def\_iitypesaved{}} \_def\_iiA #1,{\_if$#1$\_else\_def\_tmpa{#1}% \_ifx\_tmpa\_iiatsign \_ea\_iiB\_tmp,,\_else\_iindex{#1}\_fi \_ea\_iiA\_fi} \_def\_iiatsign{@} \_def\_iiB #1,{\_if$#1$\_else \_iiC#1/\_relax \_ea\_iiB\_fi} \_def\_iiC #1/#2\_relax{\_if$#2$\_else\_iindex{#2#1}\_fi} \_def\_iid #1 {\_leavevmode\_iindex{#1}#1\_futurelet\_tmp\_iiD\_def\_iitypesaved{}} \_def\_iiD{\_ifx\_tmp,\_else\_ifx\_tmp.\_else\_space\_fi\_fi} \_def\_iis #1 #2{{\_def~{ }\_global\_sdef{_,#1}{#2}}\_ignorespaces} \_def\_iitypesaved{} \_def\_iitype #1{\_def\_iitypesaved{#1}\_ignorespaces} \_public \ii \iid \iis \iitype ; \_endcode % ------------------------------------- 2020-04-21 \isempty \iffalse ... \fi added to \iindex 2020-03-26 introduced