%! Package = versionchanges %! Author = Jander Moreira \NeedsTeXFormat{LaTeX2e} \ProvidesPackage{versionchanges}[2024/05/03 v0.1 This package provides means to track changes to a document] \NewDocumentCommand{\VCVersion}{}{0.1} \NewDocumentCommand{\VCDate}{}{2024/05/03} \RequirePackage{pgfkeys} \RequirePackage{multicol} \RequirePackage{ragged2e} \RequirePackage{todonotes} \setlength{\marginparwidth}{2cm} \RequirePackage{marginnote} \let\marginpar\marginnote %% Options \pgfkeys{ /version changes/.cd, version prefix/.store in = \vc@VersionPrefix, version style/.store in = \vc@VersionStyle, change style/.store in = \vc@ChangeStyle, } \NewDocumentCommand{\VCSet}{ > { \TrimSpaces } m }{% \pgfkeys{ /version changes/.cd, #1, }% } \VCSet{ version prefix = {v}, version style = \bfseries\footnotesize, change style = \footnotesize\RaggedRight, } % defaults %% Internal commands \ExplSyntaxOn % StepChangeCounter: proceeds to a new change \int_zero_new:N \g_vc_change_counter_int \NewDocumentCommand{\vc@StepChangeCounter}{}{ \int_gadd:Nn \g_vc_change_counter_int { 1 } } \NewDocumentCommand{\vc@CurrentChangeCounter}{}{ \int_use:N \g_vc_change_counter_int } % SetChangeAttribute: sets the an attribute of the current change record % #1: attribute % #2: value \cs_new:Npn \set_change_attribute:nn #1#2 { \tl_clear_new:c { g_vc_change_ \int_use:N \g_vc_change_counter_int _#1_tl } \tl_gset:cn { g_vc_change_ \int_use:N \g_vc_change_counter_int _#1_tl } { #2 } } \NewDocumentCommand{\vc@SetChangeAttribute}{ m m }{ \set_change_attribute:nn { #1 } { #2 } } % SetChangeAttributeExpanded: sets the an attribute of the current change record % #1: attribute % #2: value \NewDocumentCommand{\vc@SetChangeAttributeExpanded}{ m m }{ \exp_args:Nnf \set_change_attribute:nn { #1 } { #2 } } % GetChangeInfo: returns the field of a change % #1: number of the change % #2: field % #3: (optional) sets macro instead of returning value \NewDocumentCommand{\vc@GetChangeInfo}{ m m o }{ \IfValueTF{#3}{ \tl_set:Nx #3 { \tl_use:c { g_vc_change_#1_#2_tl } } }{ \tl_use:c { g_vc_change_#1_#2_tl } } } % SetMacroChangeInfo: sets a macro with the field of a change % #1: macro % #2: number of the change % #3: field \NewDocumentCommand{\vc@SetMacroChangeInfo}{ m m m }{ \tl_clear_new:N #1 \exp_args:NNc \tl_set:NV #1 { g_vc_change_#2_#3_tl } } % RunChangesList: apply a macro to each change of a version % #1: version % #2: macro with a single mandatory argument \NewDocumentCommand{\vc@RunChangesList}{ m m }{ \seq_map_inline:cn { g_version_#1_seq } { #2 { ##1 } } } % RegisterVersion: add a version to the list os versions if it doesn't exist % #1: version \seq_new:N \g_vc_versions_list_seq \cs_new:Npn \register_version:nn #1#2 { \seq_if_in:NnTF \g_vc_versions_list_seq { #1 } { } {% else \seq_put_right:Nn \g_vc_versions_list_seq { #1 } \tl_clear_new:c { g_vc_version_#1_date_tl } \tl_gset:cn { g_vc_version_#1_date_tl } { #2 } \seq_new:c { g_version_#1_seq } } } \NewDocumentCommand{\VCRegisterVersion}{ m m }{ \register_version:nn { #1 } { #2 } } % VersionDate: returns the date of a version OR sets a macro % with its (expanded) value % #1: version % #2: (optional) macro to store the value \NewDocumentCommand{\vc@VersionDate}{ m o }{ \IfValueTF{#2}{ \tl_set:Nx #2 { \tl_use:c { g_vc_version_#1_date_tl } } }{ \tl_use:c { g_vc_version_#1_date_tl } } } % AddChangeToVersion: add a change reference to the list of the version % (a new list will be created if necessary) % #1: version \cs_new:Npn \add_change_to_version:n #1 { % Add a reference (change number) to the list % \tl_clear_new:N \l_change_number_tl % \exp_args:NNe \tl_set:Nn \l_change_number_tl { \int_use:N \g_vc_change_counter_int } \exp_args:Nco \seq_gput_right:Nn { g_version_#1_seq } { \int_use:N \g_vc_change_counter_int } } \NewDocumentCommand{\vc@AddChangeToVersion}{ }{ \add_change_to_version:n { \tl_use:c { g_vc_change_ \int_use:N \g_vc_change_counter_int _version_tl } } } % RunVersionList: apply a macro to each version of the list % #1: macro with a single mandatory argument \NewDocumentCommand{\vc@RunVersionList}{ m }{ \seq_map_inline:Nn \g_vc_versions_list_seq { #1 { ##1 } } } \ExplSyntaxOff %% \newif\ifvc@SuppressInListing\vc@SuppressInListingfalse \pgfkeys{ /version changes/changes/.cd, version/.code = {\vc@SetChangeAttribute{version}{#1}}, % date/.code = {\vc@SetChangeAttribute{date}{#1}}, description/.code = {\vc@SetChangeAttribute{description}{#1}}, page/.code = {\vc@SetChangeAttributeExpanded{page}{#1}}, suppress/.is if = vc@SuppressInListing, type/.is choice, type/released/.code = {\vc@SetChangeAttribute{type}{Released}}, type/new/.code = {\vc@SetChangeAttribute{type}{New in}}, type/updated/.code = {\vc@SetChangeAttribute{type}{Updated in}}, type/removed/.code = {\vc@SetChangeAttribute{type}{Removed in}}, type/deprecated/.code = {\vc@SetChangeAttribute{type}{Deprecated in}}, .unknown/.code = {\pgfkeysalso{type = \pgfkeyscurrentname}}, } % #1: (optional) todonotes options % #2: text \NewDocumentCommand{\vc@MarginNote}{ O{} > { \TrimSpaces } m }{% \todo[bordercolor = blue!20, backgroundcolor = blue!10, linecolor = blue!20, tickmarkheight = 0.2ex, size = \tiny, noline, #1]{% #2 }% } % Change: records a change % #1: (optional) todonotes options % #2: comma-separated list with change description \NewDocumentCommand{\VCChange}{ O{} > { \TrimSpaces } m o }{% \vc@StepChangeCounter% \pgfkeys{ /version changes/changes/.cd, suppress = false, type = new, page = \thepage, #2, }% \hspace{0pt}% \vc@AddChangeToVersion% %! formatter = off \ifvc@SuppressInListing% starred VCPrintChange? \vc@SetChangeAttribute{star}{*}% \else \vc@SetChangeAttribute{star}{}% \fi% %! formatter = on \vc@SetChangeAttributeExpanded{label}{\vc@CurrentChangeCounter}% \vc@GetChangeInfo{\vc@CurrentChangeCounter}{label}[\vc@InfoResult]% \expandafter\label\expandafter{\vc@InfoResult:change}% \vc@MarginNote[#1]{% \IfValueT{#3}{\textbf{#3}:\\}% \vc@GetChangeInfo{\vc@CurrentChangeCounter}{type} \vc@VersionPrefix\vc@GetChangeInfo{\vc@CurrentChangeCounter}{version}% }% } %% % Internal commands \ExplSyntaxOn % Reading from file \ior_new:N \g_vc_input_io % LoadFile: reads a previous compiled version changes \cs_new:Nn \load_file: { \tl_clear_new:N \g_vc_file_contents_tl \ior_open:NnTF \g_vc_input_io { \jobname.vcind } { \ior_map_inline:Nn \g_vc_input_io { \tl_gput_right:Nn \g_vc_file_contents_tl { ##1 } } }{} } \NewDocumentCommand{\vc@LoadChanges}{}{ \load_file: } \NewDocumentCommand{\vc@FileContentsNotEmpty}{ +m }{ \tl_if_empty:NTF \g_vc_file_contents_tl {} { #1 } } % File contents \NewDocumentCommand{\vc@FileContents}{}{ \tl_use:N \g_vc_file_contents_tl } % Writing to file \iow_new:N \g_vc_output_io \tl_new:N \g_vc_output_buffer_tl % OpenFile: open file for writing \NewDocumentCommand{\vc@OpenFile}{ }{ \iow_open:Nn \g_vc_output_io { \jobname.vcind } } % CloseFile: close file \NewDocumentCommand{\vc@CloseFile}{ }{ \iow_close:N \g_vc_output_io } % WriteBuffer: write to file \cs_generate_variant:Nn \iow_now:Nn { NV } \NewDocumentCommand{\vc@WriteBuffer}{ }{ \iow_now:NV \g_vc_output_io \g_vc_output_buffer_tl \tl_clear:N \g_vc_output_buffer_tl } \NewDocumentCommand{\vc@AddToBuffer}{ +m }{ \tl_gput_right:Nn \g_vc_output_buffer_tl { #1 } } \cs_generate_variant:Nn \tl_gput_right:Nn { Nx } \NewDocumentCommand{\vc@AddCharToBuffer}{ m }{ \tl_gput_right:Nx \g_vc_output_buffer_tl { \iow_char:N #1 } } \ExplSyntaxOff % WriteVersionChange: write to file a single change % #1: change number \NewDocumentCommand{\vc@WriteVersionChange}{ m }{% \vc@AddToBuffer{ \VCPrintChange}% \vc@SetMacroChangeInfo{\vc@InfoResult}{#1}{star}% \expandafter\vc@AddToBuffer\expandafter{\vc@InfoResult}% % version number \vc@AddCharToBuffer{\{}% \vc@SetMacroChangeInfo{\vc@InfoResult}{#1}{description}% \expandafter\vc@AddToBuffer\expandafter{\vc@InfoResult}% \vc@AddCharToBuffer{\}}% % date \vc@AddCharToBuffer{\{}% \vc@SetMacroChangeInfo{\vc@InfoResult}{#1}{page}% \expandafter\vc@AddToBuffer\expandafter{\vc@InfoResult}% \vc@AddCharToBuffer{\}}% % label \vc@AddCharToBuffer{\{}% \vc@SetMacroChangeInfo{\vc@InfoResult}{#1}{label}% \expandafter\vc@AddToBuffer\expandafter{\vc@InfoResult}% \vc@AddCharToBuffer{\}}% \vc@WriteBuffer } \NewDocumentCommand{\vc@WriteVersion}{ m }{% %! parser = off \vc@AddToBuffer{\begin{vcversionitem}}% %! parser = on \vc@AddCharToBuffer{\{}% \vc@AddToBuffer{#1}% \vc@AddCharToBuffer{\}}% \vc@AddCharToBuffer{\{}% \vc@VersionDate{#1}[\vc@InfoResult]% \expandafter\vc@AddToBuffer\expandafter{\vc@InfoResult}% \vc@AddCharToBuffer{\}}% \vc@WriteBuffer \vc@RunChangesList{#1}{\vc@WriteVersionChange}% %! parser = off \vc@AddToBuffer{\end{vcversionitem}}% %! parser = on \vc@WriteBuffer } \NewDocumentCommand{\vc@SaveChanges}{}{% \vc@OpenFile% \vc@RunVersionList{\vc@WriteVersion}% \vc@WriteBuffer \vc@CloseFile% } \NewDocumentCommand{\VCPrintChanges}{ O{} }{% \vc@FileContentsNotEmpty{ \VCSet{#1} \section*{Change History} \begin{multicols}{2} \vc@FileContents% \end{multicols} } } %% Printing the list of changes \NewDocumentEnvironment{vcversionitem}{ m m }{% \par\noindent% {\vc@VersionStyle\vc@VersionPrefix#1 (#2)} \vspace*{0.2em}\par% \begingroup% \setlength{\tabcolsep}{0pt}% \vc@ChangeStyle% }{% \endgroup% \vspace{0.5em}% } \newlength{\vc@PageNumberWidth} \setlength{\vc@PageNumberWidth}{1cm} \NewDocumentCommand{\VCPrintChange}{ s m m m }{% \IfBooleanF{#1}{% \hspace{0.25cm}% \parbox[b]{\dimexpr \linewidth - \vc@PageNumberWidth - 0.25cm}{% \setlength{\hangindent}{0.7cm}#2\dotfill% }\hspace{-0.05cm}\dotfill~\hspace{0.05cm}\hyperref[#4:change]{#3}% \par\vspace{0.5em} } } %% Version numbering \ExplSyntaxOn \tl_new:N \g_vc_prefix_text_tl \tl_new:N \g_vc_number_tl % todo: turn to int? \tl_new:N \g_vc_suffix_text_tl \regex_new:N \g_versioning_regex \regex_set:Nn \g_versioning_regex { [^\.]+ } \regex_new:N \g_subversioning_regex \regex_set:Nn \g_subversioning_regex { ([^\d]*)(\d+)(.*) } \cs_new:Npn \subversioning_check:n #1 { \seq_clear_new:N \l_subparts_seq \regex_extract_all:NnNTF \g_subversioning_regex { #1 } \l_subparts_seq { \tl_gset:Nx \g_vc_prefix_text_tl { \seq_item:Nn \l_subparts_seq {2} } \tl_gset:Nx \g_vc_number_tl { \seq_item:Nn \l_subparts_seq {3} } \tl_gset:Nx \g_vc_suffix_text_tl { \seq_item:Nn \l_subparts_seq {4} } }{ \tl_gset:Nx \g_vc_prefix_text_tl { #1 } \tl_gclear:N \g_vc_number_tl \tl_gclear:N \g_vc_suffix_text_tl } } \cs_new:Npn \versioning_check:n #1 { \seq_clear_new:N \l_parts_seq \regex_extract_all:NnN \g_versioning_regex { #1 } \l_parts_seq \seq_map_inline:Nn \l_parts_seq { \subversioning_check:n { ##1 } <<(\tl_use:N \g_vc_prefix_text_tl) (\tl_use:N \g_vc_number_tl) (\tl_use:N \g_vc_suffix_text_tl)>> } } \NewDocumentCommand{\VCVersioningCheck}{ m }{ \par\noindent[\texttt{#1}:~\versioning_check:n { #1 }]\bigskip\par } \ExplSyntaxOff % Hooks \AtBeginDocument{% \reversemarginpar% \vc@LoadChanges% } \AtEndDocument{\vc@SaveChanges}