%% This is part of the OpTeX project, see http://petr.olsak.net/optex \_codedecl \makeindex {Makeindex and sorting <2022-02-19>} % preloaded in format \_doc ----------------------------- \^`\makeindex` implements sorting algorithm at \TeX/ macro-language level. You need not any external program. The sorting can be used for various other applications, see an example in \ulink[http://petr.olsak.net/optex/optex-tricks.html\#sort]{\OpTeX/ trick~0068}. There are two passes in the sorting algorithm. The primary pass does not distinguish between a group of letters (typically non-accented and accented). If the result of comparing two string is equal in primary pass then the secondary pass is started. It distinguishes 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 given by . The is typicaly ISO 639-1 code of the language and it is declared by \~`\_preplang` defined in section~\ref[langdecl]. The \`\_sortingdatacs` for Czech rules is defined below. 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 the sorting algorithm. \_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 to be ignored are declared in \`\_ignoredchars```. See \`\_ignoredcharscs` for Czech rules. These characters are ignored in the first pass without additional condition. All characters are taken into account in the second pass: ASCII characters with code $\string<65$ are sorted first if they are not mentioned in the \^`\_sortingdata``` macro. Others not mentioned characters have undefined behavior during sorting. The compound characters (two or more characters interpreted as one character in the sorting algorithm) are mapped to single invisible characters in \`\_compoundchars```, see \`\_compoundcharscs` below for Czech rules, where ch or Ch or CH are declared as a single letter sorted between H and I. See \^`\_sortingdatacs` above where these declared characters are used. \_cod ----------------------------- \_def \_ignoredcharscs {.,;?!:'"|()[]<>=+} \_def \_compoundcharscs {ch:^^T Ch:^^U CH:^^V} % DZ etc. are sorted normally \_doc ----------------------------- You can declare these macros for more languages if you wish to use `\makeindex` with sorting rules with respect to your language. Note: if you need to map compound characters to a character, don't use `^^I`, `^^J` or `^^M` because these characters have very specific category codes. And use spaces to separate more mappings, like in \^`\_compoundcharscs` above. If you create `\_sortingdata` etc. for your language, please, send them to me. I am ready to add them to the file `lang-data.opm` in a new \OpTeX/ release. See also section~\ref[langdecl]. 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=64 \_ea\_act \_sortingdata \_relax } \_doc ----------------------------- Strings to be sorted are prepared in `\,` control sequences (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 the first character from both converted strings, compares them and if it is equal then calls itself recursively else gives the 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 a 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 with 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 current language (chosen for hyphenation patterns) is used for sorting data. If the macro \`\_sortinglang` is defined as `` (for example `\def\_sortinglang{de}` for German) then this has precedence and current 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 \_ea\_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 solve 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 other variants of indexing macros expand internally to `\iindex`. \_cod ----------------------------- \_def\_iindex#1{\_isempty{#1}\_iffalse \_openref{\_def~{ }\_ewref\_Xindex{{#1}{\_iitypesaved}}}\_fi} \_public \iindex ; \_doc ----------------------------- The \`\_Xindex``{}{}` stores `\,` to the `\_iilist` if there is the 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 the horizontal mode in 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 % ------------------------------------- 2022-02-19 \_sotringdataen etc. moved to lang-data.opm file 2021-02-15 \_expandafter -> \_ea 2021-02-01 secodary sorting: start from code 65 2020-04-21 \isempty \iffalse ... \fi added to \iindex 2020-03-26 introduced