% code-link.sty, part of the xlistings bundle % Collect definitions while typesetting and link every occurrence of them in % code listings (listings, xlistings, minted). It works without xlistings. % % Copyright (C) 2026 Florian Sihler % % This work may be distributed and/or modified under the conditions of % the LaTeX Project Public License, either version 1.3c of this license % or (at your option) any later version. The latest version of this % license is in https://www.latex-project.org/lppl.txt and version 1.3c % or later is part of all distributions of LaTeX version 2008 or later. % % This work has the LPPL maintenance status 'maintained'. % The Current Maintainer of this work is Florian Sihler. % % This work consists of the files of the xlistings bundle, among them % code-link.sty and the documentation code-link-doc.tex. \NeedsTeXFormat{LaTeX2e}[2022-06-01] \ProvidesExplPackage{code-link}{2026/11/06}{v1.1.0} {Collect, link and typeset code definitions in listings and minted} \RequirePackage{l3keys2e} \ExplSyntaxOn \msg_new:nnn { codelink } { debug } { #1 } \msg_new:nnn { codelink } { unknown-group } { Unknown~group~'#1'.~Using~the~default~group. } \msg_new:nnn { codelink } { empty-string } { Ignoring~an~empty~string. } \msg_new:nnn { codelink } { no-backend } { Backend~'#1'~requested~but~the~package~is~not~loaded. } % package options \bool_new:N \g__codelink_debug_bool \bool_new:N \g__codelink_underline_bool \bool_new:N \g__codelink_hyper_bool \bool_new:N \g__codelink_customlink_bool \tl_new:N \g__codelink_backend_tl \tl_new:N \g__codelink_hyper_tl \tl_gset:Nn \g__codelink_backend_tl { auto } \tl_gset:Nn \g__codelink_hyper_tl { auto } \tl_new:N \g__codelink_ul_offset_tl \tl_new:N \g__codelink_ul_thick_tl \tl_new:N \g__codelink_ul_color_tl \tl_gset:Nn \g__codelink_ul_offset_tl { 1.2pt } \tl_gset:Nn \g__codelink_ul_thick_tl { .3pt } \tl_gset:Nn \g__codelink_ul_color_tl { black!15 } \tl_new:N \g__codelink_ul_text_color_tl \tl_gset:Nn \g__codelink_ul_text_color_tl { black!8 } \bool_new:N \g__codelink_ul_text_bool \bool_gset_true:N \g__codelink_ul_text_bool \bool_new:N \l__codelink_ul_bool % false: no underline for this link \bool_set_true:N \l__codelink_ul_bool \keys_define:nn { codelink / package } { backend .choices:nn = { auto, listings, minted, none } { \tl_gset_eq:NN \g__codelink_backend_tl \l_keys_choice_tl }, backend .default:n = auto, hyper .choices:nn = { auto, true, false } { \tl_gset_eq:NN \g__codelink_hyper_tl \l_keys_choice_tl }, hyper .default:n = true, underline .bool_gset:N = \g__codelink_underline_bool, underline .default:n = true, underline-offset .tl_gset:N = \g__codelink_ul_offset_tl, underline-thickness .tl_gset:N = \g__codelink_ul_thick_tl, underline-color .tl_gset:N = \g__codelink_ul_color_tl, underline-text-color .tl_gset:N = \g__codelink_ul_text_color_tl, underline-text .bool_gset:N = \g__codelink_ul_text_bool, underline-text .default:n = true, debug .bool_gset:N = \g__codelink_debug_bool, debug .default:n = true, } \ProcessKeysOptions { codelink / package } \NewDocumentCommand \CodeLinkSetup { m } { \keys_set:nn { codelink / package } {#1} } % The .aux file is tokenized again, so a string cannot be written as it is: % every character becomes \codelink@c{}, which expands to it again. \cs_new:Npn \__codelink_enc:n #1 { \str_map_function:nN {#1} \__codelink_enc_char:N } \cs_new:Npn \__codelink_enc_char:N #1 { \exp_not:N \codelink@c { \int_eval:n { `#1 } } } \cs_generate_variant:Nn \__codelink_enc:n { V } \cs_new_protected:cpn { codelink@c } #1 { \char_generate:nn {#1} { 12 } } % Character renderers. listings replace the matched characters by what we give % them, so each one is typeset literally. The same shape serves the spelling of % a string for listings (spec), for Pygments (pyg) and in labels (sanitize). % A table holds one control sequence per special character, which is much % faster than a case distinction per character. % #1 table, then pairs of a character code and its replacement \cs_new_protected:Npn \__codelink_table:nnn #1#2#3 { \quark_if_recursion_tail_stop:n {#2} \cs_gset:cpn { c__codelink_ #1 _ \char_generate:nn {#2} { 12 } } { \exp_not:n {#3} } \__codelink_table:nnn {#1} } % the character is passed in braces: an undelimited argument would skip a space \cs_new:Npn \__codelink_lookup:nn #1#2 { \cs_if_exist_use:cF { c__codelink_ #1 _ #2 } {#2} } \__codelink_table:nnn { disp } { 92 } { \textbackslash { } } { 95 } { \_ } { 123 } { \{ } { 125 } { \} } { 36 } { \$ } { 35 } { \# } { 37 } { \% } { 38 } { \& } { 126 } { \textasciitilde { } } { 94 } { \textasciicircum { } } { 60 } { \textless { } } { 62 } { \textgreater { } } { 34 } { \textquotedbl { } } { 124 } { \textbar { } } { 32 } { \ } { 45 } { {-} } { 39 } { {'} } { 96 } { {`} } { 44 } { {,} } { 33 } { {!} } { 63 } { {?} } \q_recursion_tail { } \q_recursion_stop \__codelink_table:nnn { spec } { 92 } { \\ } { 32 } { \ } \q_recursion_tail { } \q_recursion_stop \__codelink_table:nnn { pyg } { 92 } { \PYGZbs { } } { 123 } { \PYGZob { } } { 125 } { \PYGZcb { } } { 94 } { \PYGZca { } } { 95 } { \PYGZus { } } { 38 } { \PYGZam { } } { 60 } { \PYGZlt { } } { 62 } { \PYGZgt { } } { 35 } { \PYGZsh { } } { 37 } { \PYGZpc { } } { 36 } { \PYGZdl { } } { 45 } { \PYGZhy { } } { 39 } { \PYGZsq { } } { 34 } { \PYGZdq { } } { 126 } { \PYGZti { } } \q_recursion_tail { } \q_recursion_stop \__codelink_table:nnn { san } { 92 } { :bs: } { 32 } { - } { 37 } { :pc: } { 35 } { :hs: } { 126 } { :ti: } { 123 } { :ob: } { 125 } { :cb: } { 94 } { :ca: } { 38 } { :am: } { 36 } { :dl: } { 34 } { :dq: } \q_recursion_tail { } \q_recursion_stop \cs_new:Npn \__codelink_disp:n #1 { \str_map_function:nN {#1} \__codelink_disp_char:N } \cs_new:Npn \__codelink_disp_char:N #1 { \__codelink_lookup:nn { disp } {#1} } \cs_new:Npn \__codelink_spec:n #1 { \str_map_function:nN {#1} \__codelink_spec_char:N } \cs_new:Npn \__codelink_spec_char:N #1 { \__codelink_lookup:nn { spec } {#1} } \cs_new:Npn \__codelink_pyg:n #1 { \str_map_function:nN {#1} \__codelink_pyg_char:N } \cs_new:Npn \__codelink_pyg_char:N #1 { \__codelink_lookup:nn { pyg } {#1} } \cs_new:Npn \__codelink_sanitize:n #1 { \str_map_function:nN {#1} \__codelink_sanitize_char:N } \cs_generate_variant:Nn \__codelink_sanitize:n { V, e } \cs_new:Npn \__codelink_sanitize_char:N #1 { \__codelink_lookup:nn { san } {#1} } % Languages are compared lowercased and without the dialect, like listings % stores \lst@language. % Results are cached, as this runs for every string and every minted environment. \regex_const:Nn \c__codelink_dialect_regex { \A \[ [^\]]* \] } \cs_new_protected:Npn \__codelink_scope:Nn #1#2 { \str_set:Nx #1 {#2} \cs_if_exist:cTF { g__codelink_sc_ #1 } { \tl_set_eq:Nc #1 { g__codelink_sc_ #1 } } { \str_set_eq:NN \l__codelink_scope_in_str #1 \str_set:Nx #1 { \str_foldcase:V #1 } \str_if_eq:eeT { \str_head:N #1 } { [ } { \regex_replace_once:NnN \c__codelink_dialect_regex { } #1 } \tl_gset_eq:cN { g__codelink_sc_ \l__codelink_scope_in_str } #1 } } \str_new:N \l__codelink_scope_in_str % Is #1 a name of letters, with an optional backslash in front? (l3regex is too % slow for a test that runs once per string) \bool_new:N \l__codelink_isword_bool \cs_new_protected:Npn \__codelink_if_word:nT #1#2 { \str_set:Nn \l_tmpb_str {#1} \str_if_eq:eeT { \str_head:N \l_tmpb_str } { \c_backslash_str } { \str_set:Nx \l_tmpb_str { \str_tail:N \l_tmpb_str } } \bool_set:Nn \l__codelink_isword_bool { ! \str_if_empty_p:N \l_tmpb_str } \str_map_inline:Nn \l_tmpb_str { \__codelink_if_letter:nF {##1} { \bool_set_false:N \l__codelink_isword_bool \str_map_break: } } \bool_if:NT \l__codelink_isword_bool {#2} } \prg_new_conditional:Npnn \__codelink_if_letter:n #1 { F } { \bool_lazy_or:nnTF { \int_compare_p:n { `A <= `#1 <= `Z } } { \int_compare_p:n { `a <= `#1 <= `z } } \prg_return_true: \prg_return_false: } % Does #1 end with a letter, a digit or an underscore? \cs_new_protected:Npn \__codelink_if_wordend:nT #1 { \exp_args:Nx \__codelink_if_wordchar:nT { \str_item:nn {#1} { -1 } } } \cs_new_protected:Npn \__codelink_if_wordchar:nT #1#2 { \bool_lazy_any:nT { { \int_compare_p:n { `A <= `#1 <= `Z } } { \int_compare_p:n { `a <= `#1 <= `z } } { \int_compare_p:n { `0 <= `#1 <= `9 } } { \int_compare_p:nNn { `#1 } = { `_ } } } {#2} } % Styles are code which runs in the group that typesets a link. \NewDocumentCommand \CodeLinkStyle { m m } { \cs_set_protected:cpn { codelink@style@ \tl_to_str:n {#1} } {#2} } \CodeLinkStyle { none } { } % Options of a string: its own, else those of its group, else the defaults. % A group is the entries "/" of one property list. \prop_new:N \g__codelink_groups_prop \cs_generate_variant:Nn \prop_get:NnNTF { Nx } \cs_generate_variant:Nn \prop_if_in:NnTF { Nx } \prop_new:N \l__codelink_opts_prop \prop_const_from_keyval:Nn \c__codelink_defaults_prop { prefix = cl:, style = none, language = , links = true, highlight = true, boundary = true } \tl_new:N \l__codelink_group_tl \tl_new:N \l__codelink_label_tl \tl_new:N \l__codelink_display_tl \tl_new:N \l__codelink_style_tl \tl_new:N \l__codelink_astyle_tl % written to the .aux file \tl_new:N \l__codelink_prefix_tl \tl_new:N \l__codelink_lang_tl \tl_new:N \l__codelink_links_tl \tl_new:N \l__codelink_hl_tl \tl_new:N \l__codelink_bd_tl \str_new:N \l__codelink_str % the string, with the backslash of a command \str_new:N \l__codelink_scope_str \bool_new:N \l__codelink_command_bool \bool_new:N \l__codelink_links_bool \keys_define:nn { codelink / entry } { group .tl_set:N = \l__codelink_group_tl, label .tl_set:N = \l__codelink_label_tl, display .tl_set:N = \l__codelink_display_tl, command .bool_set:N = \l__codelink_command_bool, command .default:n = true, prefix .code:n = { \prop_put:Nnn \l__codelink_opts_prop { prefix } {#1} }, style .code:n = { \prop_put:Nnn \l__codelink_opts_prop { style } {#1} }, language .code:n = { \prop_put:Nnn \l__codelink_opts_prop { language } {#1} }, links .code:n = { \prop_put:Nnn \l__codelink_opts_prop { links } {#1} }, highlight .code:n = { \prop_put:Nnn \l__codelink_opts_prop { highlight } {#1} }, boundary .code:n = { \prop_put:Nnn \l__codelink_opts_prop { boundary } {#1} }, } \NewDocumentCommand \CodeLinkGroup { m m } { \group_begin: \prop_clear:N \l__codelink_opts_prop \keys_set:nn { codelink / entry } {#2} \prop_map_inline:Nn \l__codelink_opts_prop { \prop_gput:Nnn \g__codelink_groups_prop { #1 / ##1 } {##2} } \group_end: \prop_gput:Nnn \g__codelink_groups_prop { #1 / } { } \cs_undefine:c { g__codelink_gc_ #1 } } \CodeLinkGroup { default } { } \cs_new_protected:Npn \__codelink_opt:nN #1#2 { \prop_get:NnNTF \l__codelink_opts_prop {#1} #2 { } { \prop_get:NxNTF \g__codelink_groups_prop { \l__codelink_group_tl / #1 } #2 { } { \prop_get:NnN \c__codelink_defaults_prop {#1} #2 } } } \cs_new_protected:Npn \__codelink_resolve: { \__codelink_opt:nN { prefix } \l__codelink_prefix_tl \__codelink_opt:nN { style } \l__codelink_style_tl \__codelink_opt:nN { language } \l__codelink_lang_tl \__codelink_opt:nN { links } \l__codelink_links_tl \__codelink_opt:nN { highlight } \l__codelink_hl_tl \__codelink_opt:nN { boundary } \l__codelink_bd_tl } % Resolves the options of the string #2 given #1. \cs_new_protected:Npn \__codelink_prepare:nn #1#2 { \tl_set:Nn \l__codelink_group_tl { default } \tl_clear:N \l__codelink_label_tl \tl_clear:N \l__codelink_display_tl \bool_set_false:N \l__codelink_command_bool \prop_clear:N \l__codelink_opts_prop \keys_set:nn { codelink / entry } {#1} \prop_if_in:NxTF \g__codelink_groups_prop { \l__codelink_group_tl / } { } { \msg_warning:nnx { codelink } { unknown-group } { \l__codelink_group_tl } \tl_set:Nn \l__codelink_group_tl { default } } \str_set:Nx \l__codelink_str { \bool_if:NT \l__codelink_command_bool { \c_backslash_str } \tl_to_str:n {#2} } % without keys of its own, a string takes the options of its group, which % are resolved once (\CodeLinkGroup drops the cache) \prop_if_empty:NTF \l__codelink_opts_prop { \cs_if_exist:cF { g__codelink_gc_ \l__codelink_group_tl } { \__codelink_resolve: \cs_gset:cpx { g__codelink_gc_ \l__codelink_group_tl } { \tl_set:Nn \exp_not:N \l__codelink_prefix_tl { \exp_not:V \l__codelink_prefix_tl } \tl_set:Nn \exp_not:N \l__codelink_style_tl { \exp_not:V \l__codelink_style_tl } \tl_set:Nn \exp_not:N \l__codelink_lang_tl { \exp_not:V \l__codelink_lang_tl } \tl_set:Nn \exp_not:N \l__codelink_links_tl { \exp_not:V \l__codelink_links_tl } \tl_set:Nn \exp_not:N \l__codelink_hl_tl { \exp_not:V \l__codelink_hl_tl } \tl_set:Nn \exp_not:N \l__codelink_bd_tl { \exp_not:V \l__codelink_bd_tl } } } \use:c { g__codelink_gc_ \l__codelink_group_tl } } { \__codelink_resolve: } \bool_set:Nn \l__codelink_links_bool { ! \str_if_eq_p:ee { \l__codelink_links_tl } { false } } % highlight=false: listings keep the colors of their own highlighting \tl_set_eq:NN \l__codelink_astyle_tl \l__codelink_style_tl \str_if_eq:eeT { \l__codelink_hl_tl } { false } { \tl_set:Nn \l__codelink_astyle_tl { none } } \tl_if_empty:NT \l__codelink_label_tl { \tl_set:Nx \l__codelink_label_tl { \l__codelink_prefix_tl \__codelink_sanitize:V \l__codelink_str } } \tl_if_empty:NT \l__codelink_display_tl { \tl_set:Nx \l__codelink_display_tl { \l__codelink_str } } \__codelink_scope:Nn \l__codelink_scope_str { \l__codelink_lang_tl } } % Registering: one line per string in the .aux file; before \begin{document} % they wait, as the file is only open then. A group with links=false registers % nothing, its strings are plain text. \seq_new:N \g__codelink_pending_seq \bool_new:N \g__codelink_document_bool \cs_new_protected:Npn \__codelink_write:n #1 { \iow_now:Nx \@mainaux {#1} } \cs_generate_variant:Nn \__codelink_write:n { V } \cs_generate_variant:Nn \seq_gput_right:Nn { NV } \cs_new_protected:Npn \__codelink_register:n #1 { \bool_if:NT \l__codelink_links_bool { \str_if_empty:NTF \l__codelink_str { \msg_warning:nn { codelink } { empty-string } } { \str_set:Nx \l_tmpa_str { g__codelink_w_ #1 / \l__codelink_scope_str / \__codelink_enc:V \l__codelink_str } \cs_if_exist:cF { \l_tmpa_str } { \cs_gset_nopar:cpn { \l_tmpa_str } { } \tl_set:Nx \l_tmpa_tl { \exp_not:N \codelink@in { \__codelink_enc:V \l__codelink_str } { \__codelink_enc:V \l__codelink_label_tl } { \__codelink_enc:V \l__codelink_display_tl } { \tl_to_str:N \l__codelink_astyle_tl } { \__codelink_enc:V \l__codelink_scope_str } {#1} { \l__codelink_bd_tl } } \bool_if:NTF \g__codelink_document_bool { \__codelink_write:V \l_tmpa_tl } { \seq_gput_right:NV \g__codelink_pending_seq \l_tmpa_tl } } } } } \cs_new_protected:Npn \__codelink_anchor_hook:nn #1#2 { } \cs_generate_variant:Nn \__codelink_anchor_hook:nn { VV } \NewDocumentCommand \CodeLinkAnchorHook { m } { \cs_set_protected:Npn \__codelink_anchor_hook:nn ##1##2 {#1} } \NewDocumentCommand \CodeLinkAnchor { O{} m } { \group_begin: \__codelink_prepare:nn {#1} {#2} \__codelink_register:n { link } \cs_if_exist:NT \phantomsection { \phantomsection } \exp_args:NV \label \l__codelink_label_tl \__codelink_anchor_hook:VV \l__codelink_str \l__codelink_label_tl \group_end: } \NewDocumentCommand \CodeLinkRegister { O{} m } { \group_begin: \__codelink_prepare:nn {#1} {#2} \__codelink_register:n { link } \group_end: } \NewDocumentCommand \CodeLinkBlock { O{} m } { \group_begin: \__codelink_prepare:nn {#1} {#2} \__codelink_register:n { block } \group_end: } % Typesetting a link. The underline is a light rule at a fixed distance below % the baseline, whatever the depth of the text is. \box_new:N \l__codelink_ul_box \box_new:N \l__codelink_ul_out_box \tl_new:N \l__codelink_ul_color_tl % the color of the line in a listing or in the text \tl_set_eq:NN \l__codelink_ul_color_tl \g__codelink_ul_color_tl \bool_new:N \l__codelink_hook_bool % set inside of the Output hook of listings \cs_new_protected:Npn \__codelink_ul_draw:N #1 { \hbox_set:Nn \l__codelink_ul_out_box { \box_use:N #1 \skip_horizontal:n { - \box_wd:N #1 } \tl_if_empty:NF \l__codelink_ul_color_tl { \@ifpackageloaded { xcolor } { \exp_args:NV \color \l__codelink_ul_color_tl } { } } \vrule width \box_wd:N #1 height \dim_eval:n { \g__codelink_ul_thick_tl - \g__codelink_ul_offset_tl } depth \dim_eval:n { \g__codelink_ul_offset_tl } \scan_stop: } \box_set_dp:Nn \l__codelink_ul_out_box { \box_dp:N #1 } \box_use_drop:N \l__codelink_ul_out_box } % #1 label (empty: no link), #2 style name, #3 text \cs_new_protected:Npn \__codelink_format:nnn #1#2#3 { \group_begin: \cs_if_exist_use:c { codelink@style@ #2 } \bool_lazy_all:nTF { { \g__codelink_underline_bool } { \l__codelink_ul_bool } { ! \tl_if_empty_p:n {#1} } } { % in the Output hook, #3 is the output of the whole token: the box % is only known once listings has made it \bool_if:NTF \l__codelink_hook_bool { \cs_set_protected:Npn \lst@OutputBox ##1 { \__codelink_ul_draw:N ##1 } #3 } { \hbox_set:Nn \l__codelink_ul_box {#3} \__codelink_ul_draw:N \l__codelink_ul_box } } {#3} \group_end: } % How a text becomes a link to a label (#1 label, #2 text): hyperref, or any % package which provides \hyperref, or the command of the user. \cs_new_protected:Npn \__codelink_linkcmd:nn #1#2 { \hyperref [#1] {#2} } \NewDocumentCommand \CodeLinkLinkCommand { m } { \cs_set_protected:Npn \__codelink_linkcmd:nn ##1##2 {#1} \bool_gset_true:N \g__codelink_customlink_bool } \NewDocumentCommand \CodeLinkFormat { m m m } { \bool_lazy_and:nnTF { \g__codelink_hyper_bool } { ! \tl_if_empty_p:n {#1} } { \__codelink_linkcmd:nn {#1} { \__codelink_format:nnn {#1} {#2} {#3} } } { \__codelink_format:nnn {#1} {#2} {#3} } } \NewDocumentCommand \CodeLinkTextFont { } { \ttfamily } \NewDocumentCommand \CodeLinkRef { O{} m } { \group_begin: \__codelink_prepare:nn {#1} {#2} \tl_set_eq:NN \l__codelink_ul_color_tl \g__codelink_ul_text_color_tl \bool_set_eq:NN \l__codelink_ul_bool \g__codelink_ul_text_bool \bool_if:NF \l__codelink_links_bool { \tl_clear:N \l__codelink_label_tl } \use:e { \exp_not:N \CodeLinkTextFont \exp_not:N \CodeLinkFormat { \exp_not:V \l__codelink_label_tl } { \exp_not:V \l__codelink_style_tl } { \__codelink_disp:V \l__codelink_display_tl } } \group_end: } \cs_generate_variant:Nn \__codelink_disp:n { V } % the label of a string, to be used in \ref and \pageref (starred: a command) \NewExpandableDocumentCommand \CodeLinkLabel { s O{default} m } { \prop_if_in:NnTF \g__codelink_groups_prop { #2 / prefix } { \prop_item:Nn \g__codelink_groups_prop { #2 / prefix } } { \prop_item:Nn \c__codelink_defaults_prop { prefix } } \__codelink_sanitize:e { \IfBooleanT {#1} { \c_backslash_str } \tl_to_str:n {#3} } } % Reading the .aux file: the entries wait until a listing needs them. % #1 string, #2 label, #3 display, #4 style, #5 language, #6 kind, #7 boundary \seq_new:N \g__codelink_new_seq \bool_new:N \g__codelink_dirty_bool \cs_new_protected:Npn \__codelink_in:nnnnnnn #1#2#3#4#5#6#7 { \group_begin: \cs_set:cpn { codelink@c } ##1 { \char_generate:nn {##1} { 12 } } \seq_gput_right:Nx \g__codelink_new_seq { { #1 } { #2 } { #3 } { \tl_to_str:n {#4} } { #5 } { \tl_to_str:n {#6} } { \tl_to_str:n {#7} } } \group_end: \bool_gset_true:N \g__codelink_dirty_bool } \cs_set_eq:cN { codelink@in } \__codelink_in:nnnnnnn % Assembling: entries which have not been seen yet become rules. The first one % of a string (per kind and language) counts. \bool_new:N \g__codelink_listings_bool \bool_new:N \g__codelink_minted_bool \cs_new_protected:Npn \__codelink_assemble: { \bool_gset_false:N \g__codelink_dirty_bool \seq_map_inline:Nn \g__codelink_new_seq { \__codelink_assemble_entry:nnnnnnn ##1 } \seq_gclear:N \g__codelink_new_seq \bool_if:NT \g__codelink_listings_bool { \__codelink_lst_sort: } } % #1 string, #2 label, #3 display, #4 style, #5 language, #6 kind, #7 boundary \cs_new_protected:Npn \__codelink_assemble_entry:nnnnnnn #1#2#3#4#5#6#7 { \cs_if_exist:cF { g__codelink_s_ #6 / #5 / #1 } { \cs_gset_nopar:cpn { g__codelink_s_ #6 / #5 / #1 } { } \str_if_eq:nnTF {#6} { block } { \tl_clear:N \l__codelink_label_tl } { \tl_set:Nn \l__codelink_label_tl {#2} } \tl_set:Nn \l__codelink_style_tl {#4} \str_if_empty:nTF {#3} { \tl_set:Nn \l__codelink_display_tl {#1} } { \tl_set:Nn \l__codelink_display_tl {#3} } \bool_if:NT \g__codelink_listings_bool { % a single word is matched by the Output hook as a whole: cheaper % than a rule, and never found inside of a longer word \bool_set_false:N \l__codelink_word_bool \str_if_eq:nnT {#6} { link } { \str_if_eq:nnT {#1} {#3} { \__codelink_if_word:nT {#1} { \bool_set_true:N \l__codelink_word_bool } } } \bool_if:NTF \l__codelink_word_bool { \__codelink_lst_word:nn {#5} {#1} } { \__codelink_lst_rule:nnn {#5} {#1} {#7} } } \bool_if:NT \g__codelink_minted_bool { \__codelink_mt_rule:nn {#5} {#1} \str_if_eq:nnT {#6} { block } { \__codelink_mt_block:nn {#5} {#1} } } } } % listings: a word is looked up when listings outputs an identifier. All other % strings are 'literate' rules, sorted by language and length (longest first). \clist_new:N \g__codelink_scopes_clist \int_new:N \g__codelink_minlen_int \int_new:N \g__codelink_maxlen_int \int_gset:Nn \g__codelink_minlen_int { 1000000 } \bool_new:N \g__codelink_words_bool \bool_new:N \l__codelink_word_bool \cs_new_protected:Npn \__codelink_lst_word:nn #1#2 { \tl_gset:cx { g__codelink_h_ x #1 _ #2 } { { \exp_not:V \l__codelink_label_tl } { \exp_not:V \l__codelink_style_tl } } \bool_gset_true:N \g__codelink_words_bool \str_if_eq:eeT { \str_item:nn {#2} { 1 } } { \c_backslash_str } { \bool_gset_true:N \g__codelink_cmdwords_bool } } % stores one literate rule: #1 language, #2 what to find, #3 the replacement \cs_new_protected:Npn \__codelink_lst_put:nnn #1#2#3 { \int_gincr:N \g__codelink_rule_int \tl_gset:cx { g__codelink_r_ \int_use:N \g__codelink_rule_int _tl } { { \__codelink_spec:n {#2} } { {#3} } { \str_count:n {#2} } } \int_gset:Nn \g__codelink_minlen_int { \int_min:nn { \g__codelink_minlen_int } { \str_count:n {#2} } } \int_gset:Nn \g__codelink_maxlen_int { \int_max:nn { \g__codelink_maxlen_int } { \str_count:n {#2} } } \clist_if_in:NnF \g__codelink_scopes_clist { x #1 } { \clist_gput_right:Nn \g__codelink_scopes_clist { x #1 } } \clist_if_exist:cF { g__codelink_b_ x #1 _ \str_count:n {#2} _clist } { \clist_new:c { g__codelink_b_ x #1 _ \str_count:n {#2} _clist } } \clist_gput_right:cx { g__codelink_b_ x #1 _ \str_count:n {#2} _clist } { \int_use:N \g__codelink_rule_int } } \int_new:N \g__codelink_rule_int % A rule matches characters, so it also fires inside of a longer word. With a % boundary, the string followed by a word character is a rule of its own which % keeps the text as it is, and the longer rule wins. \cs_new_protected:Npn \__codelink_lst_rule:nnn #1#2#3 { \__codelink_lst_put:nnn {#1} {#2} { \exp_not:N \CodeLinkFormat { \exp_not:V \l__codelink_label_tl } { \exp_not:V \l__codelink_style_tl } { \__codelink_disp:V \l__codelink_display_tl } } \str_if_eq:nnF {#3} { false } { \__codelink_if_wordend:nT {#2} { \str_map_inline:nn { abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_ } { \__codelink_lst_put:nnn {#1} { #2 ##1 } { \__codelink_disp:n { #2 ##1 } } } } } } \cs_new_protected:Npn \__codelink_lst_sort: { \clist_map_inline:Nn \g__codelink_scopes_clist { \str_set:Nn \l__codelink_bucket_str {##1} \tl_gset:cx { g__codelink_lit_ ##1 _tl } { \int_step_function:nnN { \g__codelink_minlen_int } { \g__codelink_maxlen_int } \__codelink_lst_bucket:n } } } % the rules of one length in the scope ##1 of \__codelink_lst_sort: \cs_new:Npn \__codelink_lst_bucket:n #1 { \clist_if_exist:cT { g__codelink_b_ \l__codelink_bucket_str _ #1 _clist } { \clist_map_function:cN { g__codelink_b_ \l__codelink_bucket_str _ #1 _clist } \__codelink_lst_id:n } } \cs_new:Npn \__codelink_lst_id:n #1 { \exp_not:v { g__codelink_r_ #1 _tl } } \str_new:N \l__codelink_bucket_str % Word lookup, just before listings outputs an identifier: the style of the % token is wrapped into the link. \cs_new_protected:Npn \__codelink_lst_found:N #1 { \cs_set_eq:NN \__codelink_old_style:n \lst@thestyle \exp_after:wN \__codelink_lst_found:nn #1 } \cs_new_protected:Npn \__codelink_lst_found:nn #1#2 { \cs_set:Npx \lst@thestyle ##1 { \exp_not:N \bool_set_true:N \exp_not:N \l__codelink_hook_bool \exp_not:N \CodeLinkFormat {#1} {#2} { \exp_not:N \__codelink_old_style:n { ##1 } } } } \cs_new_protected:Npn \__codelink_lst_try:n #1 { \cs_if_exist:cT { g__codelink_h_ x #1 _ \l_tmpa_str } { \exp_args:Nc \__codelink_lst_found:N { g__codelink_h_ x #1 _ \l_tmpa_str } \bool_set_true:N \l__codelink_word_bool } } % Did the last non-letter token end with a backslash? Then a command word is % looked up with the backslash in front of it. (listings spells the character % as the control sequence \lst@um\ , hence the second test.) \bool_new:N \g__codelink_afterbs_bool \bool_new:N \g__codelink_cmdwords_bool \tl_new:N \l__codelink_last_tl % the end of the token, followed by a marker: a delimited search is much faster % than taking the last characters of a string \tl_const:Nx \c__codelink_bs_tl { \c_backslash_str \exp_not:N \q_mark } \tl_const:Nx \c__codelink_bssp_tl { \c_backslash_str \c_space_tl \exp_not:N \q_mark } \cs_generate_variant:Nn \tl_if_in:nnTF { Vo } \cs_new_protected:Npn \__codelink_lst_other: { \bool_if:NT \g__codelink_cmdwords_bool { \tl_set:Nx \l__codelink_last_tl { \tl_to_str:o { \the \lst@token } \exp_not:N \q_mark } \tl_if_in:VoTF \l__codelink_last_tl { \c__codelink_bs_tl } { \bool_gset_true:N \g__codelink_afterbs_bool } { \tl_if_in:VoTF \l__codelink_last_tl { \c__codelink_bssp_tl } { \bool_gset_true:N \g__codelink_afterbs_bool } { \bool_gset_false:N \g__codelink_afterbs_bool } } } } \NewDocumentCommand \CodeLinkMarkBackslash { } { \bool_gset_true:N \g__codelink_afterbs_bool } \cs_new_protected:Npn \__codelink_lst_lookup: { \bool_set_false:N \l__codelink_word_bool \cs_if_exist:NT \lst@language { \exp_args:NV \__codelink_lst_try:n \lst@language } \bool_if:NF \l__codelink_word_bool { \__codelink_lst_try:n { } } } % A language that makes the backslash a letter (as the LaTeX one of xlistings) % spells it \lst@um\ inside of the word: that is a backslash again. % (\tl_to_str:n puts a space behind a control sequence, \token_to_str:N does not) \str_const:Nx \c__codelink_um_str { \exp_args:Nc \token_to_str:N { lst@um \c_backslash_str } \c_space_tl } \cs_new_protected:Npn \__codelink_lst_um_define:n #1 { \cs_new_protected:Npn \__codelink_lst_um:w ##1 #1 ##2 \q_mark ##3 \q_stop { \tl_if_empty:nT {##1} { \str_set:Nx \l_tmpa_str { \c_backslash_str ##2 } } } } \exp_args:NV \__codelink_lst_um_define:n \c__codelink_um_str \cs_new_protected:Npn \__codelink_lst_output: { \bool_lazy_and:nnT { \g__codelink_words_bool } { ! \str_if_eq_p:Vn \lst@codelink { false } } { \str_set:Nx \l_tmpa_str { \tl_to_str:o { \the \lst@token } } \use:x { \exp_not:N \__codelink_lst_um:w \l_tmpa_str \exp_not:N \q_mark \c__codelink_um_str \exp_not:N \q_mark \exp_not:N \q_stop } \__codelink_lst_lookup: \bool_if:NF \l__codelink_word_bool { \bool_if:NT \g__codelink_afterbs_bool { \str_put_left:Nx \l_tmpa_str { \c_backslash_str } \__codelink_lst_lookup: } } \bool_gset_false:N \g__codelink_afterbs_bool } } % The rules, handed to listings when it selects its character table. \cs_new_protected:Npn \__codelink_lst_run:N #1 { \exp_after:wN \lst@Literate #1 { } \relax \z@ } \cs_new_protected:Npn \__codelink_lst_run:n #1 { \cs_if_exist:cT { g__codelink_lit_ x #1 _tl } { \exp_args:Nc \__codelink_lst_run:N { g__codelink_lit_ x #1 _tl } } } \cs_new_protected:Npn \__codelink_lst_hook: { \str_if_eq:VnF \lst@codelink { false } { \bool_if:NT \g__codelink_dirty_bool { \__codelink_assemble: } \__codelink_lst_run:n { } \cs_if_exist:NT \lst@language { \tl_if_empty:NF \lst@language { \exp_args:NV \__codelink_lst_run:n \lst@language } } } } % minted: one control sequence per string, named like Pygments writes it \str_new:N \l__codelink_lexer_str \cs_new_protected:Npn \__codelink_mt_rule:nn #1#2 { \tl_set:Nx \l_tmpa_tl { \__codelink_pyg:n {#2} } \cs_gset_nopar:cpx { codelink@mt@ #1 @ \tl_to_str:N \l_tmpa_tl } { { \exp_not:V \l__codelink_label_tl } { \exp_not:V \l__codelink_style_tl } } } \cs_new_protected:Npn \__codelink_PYG_orig:nn #1#2 { } \cs_new_protected:Npn \__codelink_mt_found:nnn #1#2#3 { \__codelink_mt_found:nnnnn #1 {#2} {#3} } \cs_new_protected:Npn \__codelink_mt_found:nnnnn #1#2#3#4 { \CodeLinkFormat {#1} {#2} { \__codelink_PYG_orig:nn {#3} {#4} } } % A block of several tokens, such as System.out: the text of the last tokens is % kept, and a token that would be linked stays plain if this text ends with a block. \bool_new:N \g__codelink_mtblock_bool \bool_new:N \l__codelink_mtblocked_bool \int_new:N \g__codelink_mtblock_int \str_new:N \g__codelink_mt_tail_str \cs_new_protected:Npn \__codelink_mt_block:nn #1#2 { \str_set:Nx \l_tmpa_str { \__codelink_pyg:n {#2} } \seq_if_exist:cF { g__codelink_mtb_ #1 _seq } { \seq_new:c { g__codelink_mtb_ #1 _seq } } \seq_gput_right:cV { g__codelink_mtb_ #1 _seq } \l_tmpa_str \int_gset:Nn \g__codelink_mtblock_int { \int_max:nn { \g__codelink_mtblock_int } { \str_count:N \l_tmpa_str } } \bool_gset_true:N \g__codelink_mtblock_bool } \cs_new_protected:Npn \__codelink_mt_blocked:n #1 { \seq_if_exist:cT { g__codelink_mtb_ #1 _seq } { \seq_map_inline:cn { g__codelink_mtb_ #1 _seq } { \str_if_eq:eeT { \str_range:Nnn \g__codelink_mt_tail_str { - \str_count:n {##1} } { -1 } } {##1} { \bool_set_true:N \l__codelink_mtblocked_bool \seq_map_break: } } } } \cs_generate_variant:Nn \__codelink_mt_blocked:n { V } % #1 the control sequence that holds label and style, #2 and #3 the arguments of \PYG \cs_new_protected:Npn \__codelink_mt_link:nnn #1#2#3 { \bool_set_false:N \l__codelink_mtblocked_bool \bool_if:NT \g__codelink_mtblock_bool { \__codelink_mt_blocked:V \l__codelink_lexer_str \bool_if:NF \l__codelink_mtblocked_bool { \__codelink_mt_blocked:n { } } } \bool_if:NTF \l__codelink_mtblocked_bool { \__codelink_PYG_orig:nn {#2} {#3} } { \exp_args:Nv \__codelink_mt_found:nnn {#1} {#2} {#3} } } \cs_new_protected:Npn \__codelink_PYG:nn #1#2 { \bool_if:NT \g__codelink_mtblock_bool { \str_gput_right:Nx \g__codelink_mt_tail_str { \detokenize {#2} } \int_compare:nNnT { \str_count:N \g__codelink_mt_tail_str } > { 2 * \g__codelink_mtblock_int } { \str_gset:Nx \g__codelink_mt_tail_str { \str_range:Nnn \g__codelink_mt_tail_str { - \g__codelink_mtblock_int } { -1 } } } } \cs_if_exist:cTF { codelink@mt@ \l__codelink_lexer_str @ \detokenize {#2} } { \__codelink_mt_link:nnn { codelink@mt@ \l__codelink_lexer_str @ \detokenize {#2} } {#1} {#2} } { \cs_if_exist:cTF { codelink@mt@ @ \detokenize {#2} } { \__codelink_mt_link:nnn { codelink@mt@ @ \detokenize {#2} } {#1} {#2} } { \__codelink_PYG_orig:nn {#1} {#2} } } } % at the start of every minted environment \cs_new_protected:Npn \__codelink_minted_patch: { \bool_if:NT \g__codelink_dirty_bool { \__codelink_assemble: } \cs_if_exist:NT \PYG { \cs_if_eq:NNF \PYG \__codelink_PYG:nn { \cs_set_eq:NN \__codelink_PYG_orig:nn \PYG \cs_set_eq:NN \PYG \__codelink_PYG:nn } } \__codelink_scope:Nn \l__codelink_lexer_str { \minted@lexer } \str_gclear:N \g__codelink_mt_tail_str } % what is loaded decides about the backends \cs_new_protected:Npn \__codelink_begin_document: { \bool_gset_true:N \g__codelink_document_bool \iow_now:Nx \@mainaux { \exp_not:N \providecommand \exp_not:N \codelink@in [7] { } } \seq_map_function:NN \g__codelink_pending_seq \__codelink_write:n \seq_gclear:N \g__codelink_pending_seq \bool_gset:Nn \g__codelink_hyper_bool { \str_if_eq_p:Vn \g__codelink_hyper_tl { true } || ( \str_if_eq_p:Vn \g__codelink_hyper_tl { auto } && ( \cs_if_exist_p:N \hyperref || \g__codelink_customlink_bool ) ) } \str_if_in:nVT { auto listings } \g__codelink_backend_tl { \@ifpackageloaded { listings } { \bool_gset_true:N \g__codelink_listings_bool \def \lst@codelink { true } \lst@Key { codelink } { true } { \def \lst@codelink { ##1 } } \lst@AddToHook { SelectCharTable } { \__codelink_lst_hook: } \lst@AddToHook { Output } { \__codelink_lst_output: } \lst@AddToHook { OutputOther } { \__codelink_lst_other: } } { \str_if_eq:VnT \g__codelink_backend_tl { listings } { \msg_warning:nnn { codelink } { no-backend } { listings } } } } \str_if_in:nVT { auto minted } \g__codelink_backend_tl { \@ifpackageloaded { minted } { \bool_gset_true:N \g__codelink_minted_bool \AddToHook { env/MintedVerbatim/begin } { \__codelink_minted_patch: } } { \str_if_eq:VnT \g__codelink_backend_tl { minted } { \msg_warning:nnn { codelink } { no-backend } { minted } } } } \bool_if:NT \g__codelink_debug_bool { \msg_term:nnx { codelink } { debug } { listings=\bool_to_str:N \g__codelink_listings_bool,~ minted=\bool_to_str:N \g__codelink_minted_bool,~ links=\bool_to_str:N \g__codelink_hyper_bool } } } \cs_generate_variant:Nn \str_if_in:nnT { nV } \cs_generate_variant:Nn \str_if_eq:nnT { V } \AddToHook { begindocument } [ code-link ] { \__codelink_begin_document: } \ExplSyntaxOff