%<*driver>
\def\nameofplainTeX{plain}
\ifx\fmtname\nameofplainTeX
\input l3docstrip.tex
\keepsilent
\askforoverwritefalse
\preamble

========================================================================
 osgdoc tools - support for documentation

 (C) 2022-2026 Matthias Werner
 E-mail: matthias.werner@informatik.tu-chemnitz.de

 Released under the LaTeX Project Public License v1.3c or later
 See http://www.latex-project.org/lppl.txt
======================================================================== 

\endpreamble
\postamble

 This work is "maintained" (as per LPPL maintenance status) by
 Matthias Werner.
\endpostamble

\generate{
  \file{osgdoc.cls}{\from{osgdoc.dtx}{cls}}
  \file{osgdoc.sty}{\from{osgdoc.dtx}{pkg}}
}
  \expandafter\endbatchfile
\fi

\iffalse
%</driver>
%<*cls>
\NeedsTeXFormat{LaTeX2e}[2022/06/01]
\ProvidesExplClass
{osgdoc}
{2026-09-08}
{v0.5.0}
{A thin wrapper around cnltx-doc}
\ExplSyntaxOff
%</cls>
%<*driver>
\fi

\documentclass[add-bib=false]{osgdoc}
\usepackage{enumitem}
\usepackage{microtype}
\usepackage[
languages={de,en},
map={de=german,en=english},
targetlang={job=2},
load babel
]{langselect}

\newpackagename{\cnltx}{cnltx-doc}
\makeindex
\setcnltx{
  name  =  osgdoc,
  class = osgdoc,
  version  = \UseVersionOf{osgdoc.sty},
  date     = \UseDateOf{osgdoc.sty},
  title    = {\ldeen{Die \osgdoc\ Tools}{The \osgdoc\ Tools}},
  info     = {\ldeen{Unterstützung für \LaTeX-Paket-Dokumentationen}{Support for
      \LaTeX\ package documentation}},
  authors  = {Matthias Werner[matthias.werner@informatik.tu-chemnitz.de]},
  abstract={
    \ldeen{Die \osgdoc-Tools sind Teil des \pkg*{osglecture} Bundles, jedoch auch
      unabhängig von den anderen Paketen des Bundles einsetzbar.
      Sie beinhalten einerseits einen Patch-Wrapper für \name*{Clemens Niederbergers} großartige
      \cnltx-Klasse, die aber leider nicht mehr mit den aktuellen \LaTeX\
      kompatibel ist. Zum anderen werden ein paar Makros zur Unterstützung von
      Dokumentationen definiert.
    }{
      The \osgdoc\ tools are part of the \bnd{osglecture} bundle, but can also
        be used independently of the other packages in the bundle.
        They include, on the one hand, a patch wrapper for \name*{Clemens
        Niederberger}’s excellent  \cnltx\ class, which unfortunately
        is no longer compatible with the current \LaTeX\ version. On the
        other hand, a few macros to support documentations are provided.
      }
  },
  url      =https://github.com/werner-matthias/osglecture,
  build-title
}

%\OnlyDescription

\begin{document}
\section{\ldeen{Nutzung}{Usage}}
\ldeen{
  Das \osgdoc-Paket  besteht aus der Klassendatei \File{osgdoc.cls} und der
  Paketdatei \File{osgdoc.sty}. Letztere wird durch die Klasse geladen, man kann
  sie aber auch unabhängig verwenden, wenn nur die dort definierten Befehle
  gebraucht werden.
}{
  The \osgdoc package consists of the class file \File{osgdoc.cls} and the
  style file \File{osgdoc.sty}. The latter is loaded by the class, but it can
  also be used independently if only the commands defined there
  are needed.
}
\subsection{osgdoc.cls}
\ldeen{Die Klasse wird auf dem üblichen Weg geladen}{The class is loaded in the usual way}:
\cs*{documentclass}\oarg{options}\Marg{osgdoc}

\ldeen*{Die Optionen und zur Verfügung gestellten Befehle der Basisklasse @1 entnehmen Sie bitte der Dokumentation des @2-Pakets.}{
  For documentation of the options and commands provided by the base class @1, please see the @2 package documentation.}{\cnltx}{\needpackage{cnltx}}
\ldeen*{Die eigentliche Klasse @1 macht nichts, außer einige Befehle zu patchen und einige Standardeinstellungen wie das Farbschema zu ändern. }{The @1 class itself does nothing except patch a few commands and set some defaults, such as the color scheme. }{\osgdoc}
\ldeen*{Zusätzlich stehen noch die im Abschnitt~@1 beschriebenen Befehle zur Verfügung.}{%
  In addition, the commands described in Section @1 are available.}{\ref{sec:usage:sty}}

\ldeen{Die folgende Option wird sowohl von der Klasse als auch vom Paket verstanden.}{%
  The following option is understood by both the class and the package.}
\begin{options}
  \keybool{readprov}\Default{false}
  \ldeen{
    Verwendet \pkg{readprov} als Backend für \cs{UseVersionOf} und
    \cs{UseDateOf}. Die gewünschten Dateien müssen zuvor mit einem der
    \pkg{readprov}-Befehle wie \cs{ReadFileInfos} eingelesen werden.
  }{
    Uses \pkg{readprov} as the backend for \cs{UseVersionOf} and
    \cs{UseDateOf}. The requested files must first be read with one of
    \pkg{readprov}'s commands, such as \cs{ReadFileInfos}.
  }
\end{options}

\subsection{osgdoc.sty}
\label{sec:usage:sty}
\ldeen{Das Paket ergänzt folgende Befehle und Umgebungen}{This package adds the following commands and environments}:
\begin{commands}
  \command{AutoDocInput}[\oarg{mode}\marg{file}\marg{guard}]%\Default{\cs{AutoDocInput}[\Oarg{tex}\marg{file}\marg{guard}]}
  \ldeen*{
    Leichtgewichtige Möglichkeit für die Aufnahme dokumentierten Quellcodes,  analog dem @1 Mechanismus.}{
      A lightweight option for including documented source code,  similar to the @1 mechanism. 
      }{\needpackage[pkg]{doc}/\needpackage[pkg]{docstrip}}
  \ldeen*{
    Im Unterschied zu diesem sind jedoch (wie in @1) keine @2 notwendig. Außerdem
    kann Lua-Code dokumentiert und zur Textauszeichnung statt \LaTeX\ eine Art
    Mini-Markdown verwendet werden.}{
    Unlike the latter, however (as in @1), @2 is not required. In addition,
    Lua code can be documented, and a kind of Mini-Markdown can be used for text
    markup instead of \LaTeX.
  }{\pkg{gmdoc}}{\cs*{begin}\Marg{macrocode}\ldots \cs*{end}\Marg{macrocode}}

    \begin{quote}
    \begin{options}
      \opt*{mode}\Default{tex}
      \ldeen{
        Der Modus hat die Form
        \meta{Guard-Syntax}/\meta{Kommentar-Syntax}/\meta{Markup-Format}.
        Eine fehlende oder leere Kommentar-Syntax übernimmt die Guard-Syntax;
        ein fehlendes oder leeres Markup-Format ist \code{tex}.
        Damit bestimmt beispielsweise \code{lua//ascii} Lua für Guards und
        Kommentare sowie ASCII-Auszeichnung für den Dokumentationstext.
      }{
        The mode has the form
        \meta{guard syntax}/\meta{comment syntax}/\meta{markup format}.
        A missing or empty comment syntax inherits the guard syntax; a missing
        or empty markup format is \code{tex}. Thus, for example,
        \code{lua//ascii} selects Lua for guards and comments and ASCII markup
        for documentation text.
      }

      \begin{center}
      \begin{tabular}{llll}
        \textbf{\ldeen{Feld}{Field}}&\textbf{\ldeen{Bedeutung}{Meaning}}&
        \textbf{\ldeen{Werte}{Values}}&\textbf{\ldeen{Vorgabe}{Default}}\\\hline
        1&\ldeen{Guard-Syntax}{Guard syntax}&\code{tex}, \code{lua}&\code{tex}\\
        2&\ldeen{Kommentar-Syntax}{Comment syntax}&\code{tex}, \code{lua}&\ldeen{Feld 1}{field 1}\\
        3&\ldeen{Markup-Format}{Markup format}&\code{tex}, \code{ascii}&\code{tex}\\\hline
      \end{tabular}
      \end{center}

      \begin{center}
      \begin{tabular}{ll}
        \textbf{\ldeen{Eingabe}{Input}}&\textbf{\ldeen{Vollständiger Modus}{Expanded mode}}\\\hline
        \code{tex}&\code{tex/tex/tex}\\
        \code{lua}&\code{lua/lua/tex}\\
        \code{tex/lua}&\code{tex/lua/tex}\\
        \code{tex//ascii}&\code{tex/tex/ascii}\\
        \code{lua//ascii}&\code{lua/lua/ascii}\\
        \code{lua/tex/ascii}&\code{lua/tex/ascii}\\\hline
      \end{tabular}
      \end{center}
      \ldeen{
        Da das zweite Feld die Kommentar-Syntax bezeichnet, ist beispielsweise
        \code{tex/ascii} kein Kürzel für ASCII-Text; hierfür ist
        \code{tex//ascii} zu verwenden.
      }{
        Because the second field denotes the comment syntax, \code{tex/ascii}
        is not shorthand for ASCII text; use \code{tex//ascii} instead.
      }
      \ldeen{Für die einzelnen Elemente gilt folgende Syntax:}{The following syntax applies to
       the individual elements:}

      %\begin{quote}
        \textbf{Guards}
        \begin{itemize}[nosep]
          \item[\option{tex}]: \code{\%<*\meta{Guard}>}~/~\code{\%</\meta{Guard}>}
          \item[\option{lua}]: \code{--<*\meta{Guard}>}~/~\code{--</\meta{Guard}>}
        \end{itemize}
        \textbf{\ldeen{Kommentare}{Comments}}
        \begin{itemize}[nosep]
          \item[\option{tex}]: \ldeen{genau eine \code{\%}-Marke, optional
            gefolgt von einem Leerzeichen. Zeilen mit \code{\%\%} oder mehr
            Prozentzeichen werden als Quelltext dargestellt}{exactly one
            \code{\%} character, optionally followed by a space. Lines with
            \code{\%\%} or more percent characters are rendered as source code}
          \item[\option{lua}]: \ldeen{mindestens zwei \code{-}-Marken,
            optional gefolgt von einem Leerzeichen, oder ein Lua-Langkommentar}{
            at least two \code{-} characters, optionally followed by a space,
            or a Lua long comment}
        \end{itemize}
      \textbf{\ldeen{Auszeichnung}{Markup}}
      \begin{itemize}[nosep]
        \item[\option{tex}]: \ldeen{\LaTeX, wobei für eine  bessere Kompatibilität mit \LaTeX3 nicht maskierte Unterstriche innerhalb der Argumente von \cs{cs} und \cs{cs*} maskiert werden.
        Dadurch können beispielsweise \cs{keys\_set:nn} und
        \cs*{\_\_module\_function:N} direkt geschrieben werden.
        Der Makroname muss vollständig in einer Zeile stehen.}{\LaTeX, where, for better
        compatibility with \LaTeX3, unmasked underscores within the arguments of \cs{cs} and
        \cs{cs*} are masked.
        This allows, for example, \cs{keys\_set:nn} and
        \cs*{\_\_module\_function:N} to be written directly.
        The macro name must appear entirely on a single line.}
        \item[\option{ascii}]: \ldeen{Formatierung in einer kleinen, zeilenorientierten
        Auszeichnungssprache.}{Formatting in a small, line-oriented
        markup language.}
        \begin{description}[nosep]
        \item \code{*text*}: \textbf{text}
        \item \code{\_text\_}:  \emph{text}
        \item \code{`code`}: \code{code}
        \item \code{\textbackslash macro} \ldeen{oder}{or}
          \code{`\textbackslash macro`}:
          \cs{macro}
        \item \code{===\ldots}: \rule{2cm}{1pt} (\ldeen{Trennlinie}{rule})
        \item \code{\#}/\code{\#\#}: (\ldeen{am Zeilenanfang mit folgendem Leerzeichen}{
          at the beginning of the line, followed by a space}): \textbf{\ldeen{Überschriften}{headings}}
        \item \code{-}/\code{*} (\ldeen{am Zeilenanfang mit folgendem Leerzeichen}{at the beginning
        of the line, followed by a space}): \textbullet~\ldeen{Listenpunkte}{list items}
        \end{description}
      \end{itemize}
      \opt*{file}\ldeen{Name der Datei, die eingebunden werden soll.}{Name of file to include.}
      \opt*{guard}\ldeen{
        Name des Guards. Es sind nur einfache Namen zulässig, keine komplexen Guardausdrücke.}{
        Name of the guard. Only simple names are allowed here, no complex guard expressions.}
    \end{options}
  \end{quote}
  \command{OnlyDescription}\default{}
  \command{MaybeStop}[\marg{text}]
    \ldeen{Die beiden Makros erfüllen die gleiche Funktion wie die gleichnamigen Makros aus dem \pkg{doc}-Paket}{These macros perform the same functions as the macros of the same name from the \pkg{doc} package}.
  \command{UseVersionOf}[\marg{file}]\default{}%\vspace{-.7\baselineskip}
  \command{UseDateOf}[\marg{file}]
    \ldeen{
      Liest die durch \meta{file} angegebene Datei und sucht nach \cs{ProvidesExplPackage}, \cs{ProvidesPackage}, \cs{ProvidesExplClass}, \cs{ProvidesClass}, \cs{ProvidesExplFile} oder \cs{ProvidesFile} und gibt die gesuchte Information zurück (Version oder Datum). 
    }{
      Reads the file specified by \meta{file} and searches for \cs{ProvidesExplPackage}, \cs{ProvidesPackage}, \cs{ProvidesExplClass}, \cs{ProvidesClass}, \cs{ProvidesExplFile}, or \cs{ProvidesFile}, and returns the information being sought (version or date). 
    }
\end{commands}

\begin{environments}
  \environment{infobox}[\marg{title}]\default{}
  \environment{warnbox}[\marg{title}]
    \ldeen{Kästchenumgebungen für Information und Warnungen}{Boxed text blocks for information and warnings}.
\end{environments}

\section{\ldeen{Bugs und Einschränkungen}{Bugs and Limitations}}
\begin{itemize}
\item \ldeen{\osgdoc\ ist zum gegenwärtigen Zeitpunkt als experimentell zu betrachten,
  die Versionsnummer weist darauf hin.}{\osgdoc\ should be regarded as
  experimental at this stage, as the version number indicates.}
\item \ldeen{
    Die \osgdoc-Klasse ist nur ein dünner Wrapper für die
    {\cnltx}-Klasse aus dem \pkg{cnltx}-Paket. Sie sorgt nur für
    syntaktische Korrektheit im 
    Standardfall. Es ist durchaus möglich, dass bei der Vielzahl von Optionen
    und Möglichkeiten, die \cnltx\ bietet, weitere Probleme mit dem
    heutigen \LaTeX\ existieren, die \osgdoc\ nicht abfängt.
  }{
    The \osgdoc\ class is merely a thin wrapper for the
    \cnltx\ class. It ensures syntactic correctness only in
    the standard case. Given the multitude of options
    and possibilities offered by \cnltx, it is entirely possible that
    there are additional issues with 
    today’s \LaTeX\ that \osgdoc\ does not handle.
  }
\item \ldeen*{
    Die Dateiinformation bei den klassischen (nicht \LaTeX{}3)
    \cs*{Provide\ldots}-Befehlen beruhen auf einen einfachen Lua-Musterabgleich über dem
    optionalen Argument. Wenn sicher ist, dass Dateien \emph{nicht} im \LaTeX{}3-Syntax
    geschrieben sind, kann mit der Option @1 stattdessen @2 benutzt werden.
  }{
    The file information for the classic (non-LaTeX3)
    \cs*{Provide\ldots} commands is based on a simple Lua pattern match using the
    optional argument. If you are certain that files are \emph{not} written in LaTeX3 syntax,
    option @1 can be used to select @2 instead.
  }{\code{readprov}}{\needpackage*[tex-archive/macros/latex/contrib/fileinfo]{readprov}}

  \item \ldeen{Obwohl das \bnd*{osglecture}-Bundle bemüht ist, die Barrierefreiheit mittels Tagging zu
    unterstützen, erzeugt \osgdoc\ \emph{keine} korrekt getaggten PDFs.}{Although the \bnd*{osglecture} bundle
    strives to support accessibility through tagging,
    \osgdoc\ does \emph{not} generate correctly tagged PDFs.}
\end{itemize}
\section{Implementation}
\MaybeStop{\ldeen*{Um die Implementationsbeschreibung zu erhalten, kommentieren Sie bitte \cs{OnlyDescription} in \code{osgdoc.dtx} aus.}{To view the implementation description, please uncomment \cs{OnlyDescription} in \File{osgdoc.dtx}.}}

\subsection{Class \osgdoc.cls}
\AutoDocInput{osgdoc.dtx}{cls}
\subsection{Package \osgdoc.sty}
\AutoDocInput{osgdoc.dtx}{pkg}
\end{document}
%</driver>
%<*cls>
\ExplSyntaxOn
% \ldeen*{Zunächst müssen wir @1 patchen. Dazu muss der nicht mehr gültige @2-Ruf abgefangen 
%   werden.
% }{
% First, we need to patch @1. To do that, we need to intercept the @2 call, which is no longer valid. 
% }{\cls*{cnltx-doc}}{\cs{AfterPackage!}}
\AddToHook{class/scrartcl/after}{
  \NewCommandCopy{\originalAfterPackage}{\AfterPackage}
  \RenewDocumentCommand{\AfterPackage}{t! m m}{
    \IfBooleanTF{#1}{#3}{%
      \originalAfterPackage{#2}{#3}
    }
  }
}
\AddToHook{class/cnltx-doc/before}{
  \ClassInfo{osgdoc}{Patches~applied~for~cnltx-doc.cls}
}
% \ldeen*{Die Pakete @1 und @2 müssen explizit geladen werden, um nicht den Fehler "`\emph{@3}"' zu triggern.}{
%  The packages @1 and @2 must be explicitly loaded to avoid triggering "`\emph{@3}"' error .
% }{\pkg{multicol}}{\pkg{ragged2e}}{Loading in a group}
\RequirePackage{multicol,ragged2e}
\sys_if_engine_luatex:T{
  \RequirePackage{luatex85}
}
% \ldeen*{Die @1-Umgebung braucht @2.}{The @1 environment needs @2.}{\env{example}}{\pkg{shellesc}}
\RequirePackage{shellesc}
%
% \ldeen*{
%   @1 versucht für die Logo-Kerning-Anpassung an @2 das (nichtveröffentlichte) Paket @3
%   zu laden. Dies wird per Dateisubstitution und unterbunden und  stattdessen @4 genutzt,
%   das dieselbe Aufgabe für viele Schriften übernimmt.
% }{
%   @1 attempts to load the (unpublished) package @3 to adjust the logo kerning to @2.
%   This is prevented via file substitution, and @4 is used instead,
%   which performs the same task for many fonts.
% }{\cls*{cnltx-doc}}{\pkg{libertine}}{\pkg{libertinehologopatch}}{\pkg{metalogox}}
\declare@file@substitution{libertinehologopatch.sty}{}
% \ldeen*{
%   Die Option @2 von @1 (oder eine ihrer Varianten) lädt @3 und mit fest einprogrammierten 
%   Optionen. Das kollidiert, sobald das Dokument selbst (etwa über @4) @3 mit anderen
%   Optionen laden will. 
%   Wir unterbinden daher den Ladeversuch temporär per Dateisubstitution
%   und holen ihn mit denselben von @1 konfigurierten Optionen erst am Ende
%   der Präambel nach, und zwar nur dann, wenn bis dahin noch niemand @3 (oder
%   @5) geladen hat. 
% }{
%   Option @2 of @1 (or one of its variants) loads @3 with hard-coded options. 
%   This causes a conflict as soon as the document itself (for example, via @4) attempts 
%   to load @3 with different options.
%   We therefore temporarily prevent the attempt to load @3 via file substitution
%   and load it with the same options configured by @1 only at the end
%   of the preamble—and only if no one has loaded @3 (or @5) by that point.
% }{\cls*{cnltx-doc}}{\code{load-preamble}}{\pkg{babel}}{\pkg{langselect}}{\pkg{polyglossia}}
\declare@file@substitution{babel.sty}{}

% \ldeen*{
%   Damit @1s eigene Font-/Microtype-/Babel-Logik (Option @2) unverändert
%   greift, wird sie als Vorgabe an @1 durchgereicht, ebenso alle weiteren vom
%   Anwender explizit übergebenen Optionen (etwa @3, @4, @5).
% }{
%   To ensure that @1's own font/Microtype/Babel logic (option @2) remains unchanged,
%   it is passed on to @1 as the default, as are all other options explicitly provided
%   by the user (such as @3, @4, @5).
% }{\cls*{cnltx-doc}}{\code{load-preamble+}}{\code{load-fonts=false}}{\code{add-index}}{\code{babel-options}}
\PassOptionsToClass{load-preamble+}{cnltx-doc}
\DeclareOption{readprov}{\PassOptionsToPackage{readprov}{osgdoc}}
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{cnltx-doc}}
\ProcessOptions\relax
\LoadClass{cnltx-doc}
\undeclare@file@substitution{babel.sty}
% \ldeen*{
%   @1s nutzt @3 für die Redefinition von @2 ohne Bereitstellung von @4.
%   Wir stellen einen minimalen Ersatz bereit.
% }{
%   @1s uses @3 to redefine @2 without providing @4.
%   We provide a minimal replacement.
% }{\cls*{cnltx-doc}}{\cs{@makefnmark}}{\cs{textsu}}{\pkg{superiors}}
\providecommand\textsu[1]{\textsuperscript{#1}}
% \ldeen*{
%   Der Ersatz für @1: @2 erkennt die aktive Schrift (hier @3) automatisch und
%   wählt ein passendes Kerning für die \TeX-Familie-Logos.
% }{
%   The replacement for @1: @2 automatically detects the active font (here
%   @3) and selects matching kerning for the \TeX-family logos.
% }{\pkg{libertinehologopatch}}{\pkg{metalogox}}{\pkg{libertine}}
\RequirePackage{metalogox}
% \ldeen*{
%   Das eigentliche Nachladen von @1, siehe oben.
% }{
%   The actual deferred loading of @1, see above.
% }{\pkg{babel}}
\AtEndPreamble{
  \@ifpackageloaded{babel}{}{
    \@ifpackageloaded{polyglossia}{}{
      \ifbool{cnltx@load@preamble}{
        \RequirePackage[\cnltx@babel@options]{babel}
      }{}
    }
  }
}
\RequirePackage[hidelinks]{hyperref}
\RequirePackage{osgdoc}
\RequirePackage{imakeidx}

% \ldeen{
%   Manchmal ist ein Paket Teil eines Bundles, so dass das Verzeichnis einen anderen Namen hat.
%   Für diese Fälle wird für die \cs*{needXXX}-Macros zusätzlich eine Sternvariante zur 
%   Verfügung gestellt, die bei der URL
%   nur das optionale Argument (Pfad) ausgibt.
%   Außerdem sollte nur noch https genutzt werden.
% }{
%   Sometimes a package is part of a bundle, thus the directory has a different name.
%   In these cases, an starred variant of the \cs*{needXXX} macros is also provided, which, in the URL,
%   outputs only the optional argument (path). 
%   Also, today should https be used only.
% }
\DeclareTranslationFallback{on-CTAN}{on~\ctan{}~as}
\DeclareTranslation{English}{on-CTAN}{on~\ctan{}~as}
\DeclareTranslation{German}{on-CTAN}{auf~\ctan{}~als}
\RenewDocumentCommand{\CTANurl}{s O{tex-archive/macros/latex/contrib} m}{
  \GetTranslation{on-CTAN}~\cnltx@weblink@font{#3}:~
  \IfBooleanTF{#1}{
    \url{https://ctan.org/#2}
  }{
    \url{https://ctan.org/#2/#3}
  }
}

\RenewDocumentCommand{\needpackage}{s O{tex-archive/macros/latex/contrib} m}{
  \pkg{#3}\footnote{
    \IfBooleanTF{#1}{
      \CTANurl*[#2]{#3}
    }{
      \CTANurl[#2]{#3}
    }
  } 
}

\RenewDocumentCommand{\needclass}{s O{tex-archive/macros/latex/contrib} m}{
  \cls{#3}\footnote{
    \IfBooleanTF{#1}{
      \CTANurl*[#2]{#3}
    }{
      \CTANurl[#2]{#3}
    }
  } 
}

% \ldeen{
%   Da die Titellei symbolische Fußnotenzeichen nutzt, wird die Nummer zurückgesetzt.
% }{
%   Since the title uses symbolic footnote signs, we reset the number.
% }
\AddToHook{cmd/cnltx@title@information/after}{
  \setcounter{footnote}{0}
}
\def\gobble{\@gobble}
%</cls>
%<*pkg>
\NeedsTeXFormat{LaTeX2e}[2022/06/01]
\ProvidesExplPackage
{osgdoc}
{2026-09-08}
{v0.5.0}
{small tools for documentation}
\newif\ifosgdoc@readprov
\DeclareOption{readprov}{\osgdoc@readprovtrue}
\ProcessOptions\relax
\ifosgdoc@readprov
  \RequirePackage{readprov}
\fi
% \ldeen{Wir beginnen mit den Funktionalitäten, die nicht Lua braucht.}{
% We'll start with the features that doesn't require Lua.}
% \subsection*{\ldeen{Farbboxen}{Color Boxes}}
\RequirePackage{tcolorbox}
\newtcolorbox{warnbox}[1]{
  colback=red!5!white,
  colframe=red!75!black,fonttitle=\bfseries,
  title={#1}
}
\newtcolorbox{infobox}[1]{
  colback=cnltxformalblue!5!white,
  colframe=cnltxformalblue,fonttitle=\bfseries,
  title={#1}
}
% \subsection*{\ldeen{Farbboxen}{Color Boxes}}
\newbool{osgdocimpl}
\booltrue{osgdocimpl}
\NewDocumentCommand{\OnlyDescription}{}{
  \boolfalse{osgdocimpl}
}
\NewDocumentCommand{\MaybeStop}{+m}{
  \ifbool{osgdocimpl}{
    \def\osgterminate{\relax}
  }{
    #1
    \def\osgterminate{\end{document}\endinput}
  }
\osgterminate
}
% \ldeen*{Wenn die @1-Klasse (und damit auch die @2-Klasse) nicht geladen ist, sollte das @3-Paket geladen werden.}{
%  If there is no @1 class, and thus no @2 class, the package @3 should be loaded.}
% {\cls{osgdoc}}
% {\cls{cnltx-doc}}
% {\pkg{cnltx}}
\@ifclassloaded{osgdoc}{}{
  \RequirePackage[example,names]{cnltx}
}
% \ldeen{Noch ein paar Einstellungen, die ich bevorzuge.}{A few more settings that I prefer.}
\mdfdefinestyle{cnltxmdframed}{
  backgroundcolor = cnltxbg ,
  linecolor = cnltx ,
  roundcorner = 5pt
}
\newcommand\osgdoc@example@pre{%
 \begin{mdframed}[
    backgroundcolor=white,
    hidealllines=true,
    innerleftmargin=0pt,
    innerrightmargin=0pt,
    innertopmargin=0pt,
    innerbottommargin=0pt
  ]
}
\newcommand\osgdoc@example@after{%
  \end{mdframed}
} 
\AddToHook{env/example/begin}{
  \setcnltx{
    pre-output   = \osgdoc@example@pre,
    after-output = \osgdoc@example@after,
  }%
}
\setcnltx{
  color-scheme=blue
}
\renewcommand{\packageformat}{\color{cnltxformalblue}\sffamily}
\renewcommand{\classformat}{\color{cnltxformalblue}\sffamily}
\renewcommand{\bundleformat}{\color{cnltxformalblue}\sffamily}
% \ldeen{Zusätzlich zu den Befehlen wie \cs{cls} oder \cs{pkg} wird \cs{File} definiert. Das Macro wird entgegen der 
% Konvention groß geschrieben, um einen Clash mit dem \pkg*{dirtree}-Paket zu vermeiden.}{
% In addition to commands such as \cs{cls} or \cs{pkg}, \cs{File} is also defined. It is capitalized, contrary 
% to convention, to avoid a conflict with the \pkg*{dirtree} package.}

\newcommand*{\filefomat}{\color{cnltxformalblue}\ttfamily}
\newcommand{\File}[1]{{\filefomat#1}}
% \ldeen{Die restlichen Funktionen brauchen Lua. Um keine harten Fehler zu generieren, werden Stubs 
% zur Verfügung gestellt.}{The remaining features require Lua. To avoid generating critical errors, 
% stubs are provided.}
\ExplSyntaxOn
\let \ifLuaTF=\sys_if_engine_luatex:TF
\ExplSyntaxOff
\def\odnext{\relax}
\ifLuaTF{
  \RequirePackage{luacode}
}{
  \NewDocumentCommand{\AutoDocInput}{O{tex} m m}{
    \textcolor{red}{For use \string\AutoDocInput, you have to translate this document with Lua\LaTeX.}
  }
  \ifosgdoc@readprov\else
    \NewDocumentCommand{\UseVersionOf}{m}{
      \textcolor{red}{For use \string\UseVersionOf, you have to translate this document with Lua\LaTeX.}
    }
    \NewDocumentCommand{\UseDateOf}{m}{
      \textcolor{red}{For use \string\UseDateOf, you have to translate this document with Lua\LaTeX.}
    }
  \fi
  \let\odnext\endinput
}
\odnext

% \subsection*{AutoDocInput}
% \ldeen{Für \cs{AutoDocInput} brauchen wir neben \pkg{luacode} für das Parsing noch
% \pkg{fancyverb}/\pkg{fvextra} für die Darstellung}{For \cs{AutoDocInput}, we need beside \pkg{luacode}
% for parsing in addition \pkg{fancyverb}/\pkg{fvextra} for rendering}.
\RequirePackage{fvextra}
\fvset{formatcom=\color{blue},numbers=left, breaklines, breakafter=~\{\ }
\begin{luacode*}
  function texassert(v, msg)
    if v then
      return v
    end
    tex.error("Error", {msg or "assertion failed"})
  end
\end{luacode*}
%
% \cs{AutoDocInput}\oarg{mode}\marg{file}\marg{guard}
\NewDocumentCommand{\AutoDocInput}{O{tex} m m}{
  \directlua{
    autodoc.autodoc_process(
      \luastring{#2},
      \luastring{#3},
      \luastring{#1}
    )
  }
}

\begin{luacode*}
autodoc = autodoc or {}

local verbtempfiles = {}
% \ldeen{Syntaxmodi}{Syntax modes}
-- TODO: mode auto?
local function get_syntax(mode)
  mode = mode or "tex"
  local fields = {}

  for field in (mode .. "/"):gmatch("(.-)/") do
    fields[#fields + 1] = field
  end

  local function invalid(message)
    tex.error(
      "AutoDocInput: invalid syntax mode `" .. tostring(mode) .. "'",
      {
        message,
        "Expected: guard/comment/format",
        "Guard and comment syntax: tex or lua",
        "Text format: tex or ascii",
      }
    )

    return {
      guard_prefix = "%%",
      comment_kind = "tex",
      documentation_format = "tex",
    }
  end

  if #fields > 3 then
    return invalid("A syntax mode has at most three fields.")
  end

  local guard_kind = fields[1]
  if guard_kind == nil or guard_kind == "" then
    guard_kind = "tex"
  end

  local comment_kind = fields[2]
  if comment_kind == nil or comment_kind == "" then
    comment_kind = guard_kind
  end

  local documentation_format = fields[3]
  if documentation_format == nil or documentation_format == "" then
    documentation_format = "tex"
  end

  if guard_kind ~= "tex" and guard_kind ~= "lua" then
    return invalid("Unknown guard syntax `" .. guard_kind .. "'.")
  end

  if comment_kind ~= "tex" and comment_kind ~= "lua" then
    return invalid("Unknown comment syntax `" .. comment_kind .. "'.")
  end

  if documentation_format ~= "tex" and documentation_format ~= "ascii" then
    return invalid(
      "Unknown documentation text format `" .. documentation_format .. "'."
    )
  end

  return {
    guard_prefix = guard_kind == "tex" and "%%" or "%-%-",
    comment_kind = comment_kind,
    documentation_format = documentation_format,
  }
end

% \ldeen{Helper für Lua-Patterns}{Lua-pattern helpers}
local function quote_pattern(text)
  return (text:gsub("(%W)", "%%%1"))
end

local function guard_contains_symbol(expression, symbol)
  if not expression then
    return false
  end

  local atomchars = "%w_%-"
  local quoted_symbol = quote_pattern(symbol)

  local pattern =
    "%f[" .. atomchars .. "]"
    .. quoted_symbol
    .. "%f[^" .. atomchars .. "]"

  return expression:find(pattern) ~= nil
end

% \ldeen{Guarderkennung}{Guard recognition}
local function guard_expression(line, syntax, guard_kind)
  local operator

  if guard_kind == "open" then
    operator = "%*"
  elseif guard_kind == "close" then
    operator = "/"
  else
    error("Unknown guard kind: " .. tostring(guard_kind))
  end

  return line:match(
    "^%s*"
    .. syntax.guard_prefix
    .. "%s*<%s*"
    .. operator
    .. "%s*(.-)%s*>%s*$"
  )
end

local function is_opening_guard(line, syntax, symbol)
  local expression = guard_expression(line, syntax, "open")
  return guard_contains_symbol(expression, symbol)
end

local function is_closing_guard(line, syntax, symbol)
  local expression = guard_expression(line, syntax, "close")
  return guard_contains_symbol(expression, symbol)
end

% \ldeen{Erkennung von Kommentaren}{Documentation-comment recognition}
local function tex_comment_text(line)
  -- A guard is not a documentation comment.
  if line:match("^%s*%%%s*<") then
    return nil
  end

  local markers, text = line:match("^%s*(%%+)%s?(.*)$")

  if markers == "%" then
    return text
  end

  return nil
end

autodoc.tex_comment_text = tex_comment_text

local function lua_line_comment_text(line)
  -- Again: a Lua guard is not a documentation comment.
  if line:match("^%s*%-%-%s*<") then
    return nil
  end

% \ldeen{Lua-Langkommentare werden gesondert behandelt}{Lua long-comment openings are handled separately.}
  if line:match("^%s*%-%-%[(=*)%[") then
    return nil
  end

  if line:match("^%s*%-%-%[%s*$") then
    return nil
  end

  return line:match("^%s*%-%-+%s?(.*)$")
end

local function comment_text(line, syntax)
  if syntax.comment_kind == "tex" then
    return tex_comment_text(line)
  end

  return lua_line_comment_text(line)
end

% \ldeen{Lua-Langkommentare. Folgende Varianten werden erkannt:}{Lua long comments. 
% Recognised forms:}
% \begin{itemize}[nosep]
%   \item \texttt{--[[} \ldots \texttt{]]}
%   \item  \texttt{--[=[} \ldots \texttt{]=]}
%   \item \texttt{--[==[} \ldots \texttt{]==]}
% \end{itemize}

local function lua_long_comment_start(line)
  local equals, rest =
    line:match("^%s*%-%-%[(=*)%[%s?(.*)$")

  if equals ~= nil then
    return {
      pseudo = false,
      equals = equals,
      text = rest,
    }
  end

  local rest_pseudo =
    line:match("^%s*%-%-%[%s?(.*)$")

  if rest_pseudo ~= nil then
    return {
      pseudo = true,
      equals = "",
      text = rest_pseudo,
    }
  end

  return nil
end

local function lua_long_comment_line(line, long_comment)
  if long_comment.pseudo then
    local text = line:match("^(.-)%s*%-%-%]%s*$")

    if text ~= nil then
      return text, true
    end

    return line, false
  end

  local delimiter =
    "%]"
    .. quote_pattern(long_comment.equals)
    .. "%]"

  local text =
    line:match("^(.-)%s*" .. delimiter .. "%s*$")

  if text ~= nil then
    return text, true
  end

  return line, false
end

% \ldeen{Nicht maskierte Unterstriche in \cs{cs}-Argumenten maskieren.}
%   {Escape unescaped underscores in \cs{cs} arguments.}
local function is_escaped(text, position)
  local backslashes = 0
  local index = position - 1

  while index >= 1 and text:sub(index, index) == "\\" do
    backslashes = backslashes + 1
    index = index - 1
  end

  return backslashes % 2 == 1
end

local function skip_spaces(text, position)
  while position <= #text and text:sub(position, position):match("%s") do
    position = position + 1
  end

  return position
end


local function skip_balanced(text, position, opening, closing)
  local depth = 0

  for index = position, #text do
    local character = text:sub(index, index)

    if not is_escaped(text, index) then
      if character == opening then
        depth = depth + 1
      elseif character == closing then
        depth = depth - 1

        if depth == 0 then
          return index + 1
        end
      end
    end
  end

  return nil
end


local function escape_cs_underscores(text)
  local result = {}
  local cursor = 1
  local search = 1

  while search <= #text do
    local command_start, after_name = text:find("\\cs", search, true)

    if command_start == nil then
      break
    end

    local following = text:sub(after_name + 1, after_name + 1)

    if is_escaped(text, command_start)
      or following:match("[%a@]")
    then
      search = after_name + 1
    else
      local position = skip_spaces(text, after_name + 1)

      if text:sub(position, position) == "*" then
        position = skip_spaces(text, position + 1)
      end

      if text:sub(position, position) == "[" then
        position = skip_balanced(text, position, "[", "]")

        if position == nil then
          break
        end

        position = skip_spaces(text, position)
      end

      if text:sub(position, position) ~= "{" then
        search = after_name + 1
      else
        local argument_end = skip_balanced(text, position, "{", "}")

        if argument_end == nil then
          break
        end

        result[#result + 1] = text:sub(cursor, position)

        for index = position + 1, argument_end - 2 do
          local character = text:sub(index, index)

          if character == "_" and not is_escaped(text, index) then
            result[#result + 1] = "\\"
          end

          result[#result + 1] = character
        end

        result[#result + 1] = "}"
        cursor = argument_end
        search = argument_end
      end
    end
  end

  result[#result + 1] = text:sub(cursor)
  return table.concat(result)
end

% \ldeen{Kleine ASCII-Auszeichnungssprache für Dokumentationskommentare.}
%   {Small ASCII markup language for documentation comments.}
local function ascii_escape_text(text)
  local replacements = {
    ["#"] = "\\#",
    ["$"] = "\\$",
    ["%"] = "\\%",
    ["&"] = "\\&",
    ["{"] = "\\{",
    ["}"] = "\\}",
    ["~"] = "\\textasciitilde{}",
    ["^"] = "\\textasciicircum{}",
    ["_"] = "\\_",
  }

  return (text:gsub("[#%$%%&{}~^_]", replacements))
end

local function ascii_escape_code(text)
  local replacements = {
    ["\\"] = "\\textbackslash{}",
    ["#"] = "\\#",
    ["$"] = "\\$",
    ["%"] = "\\%",
    ["&"] = "\\&",
    ["{"] = "\\{",
    ["}"] = "\\}",
    ["~"] = "\\textasciitilde{}",
    ["^"] = "\\textasciicircum{}",
    ["_"] = "\\_",
  }

  return (text:gsub("[\\#%$%%&{}~^_]", replacements))
end

local function ascii_delimiter_can_open(text, position, marker)
  if marker == "`" then
    return true
  end

  local before = text:sub(position - 1, position - 1)
  local after = text:sub(position + 1, position + 1)
  return not before:match("[%w_]") and not after:match("%s")
end

local function ascii_delimiter_can_close(text, position, marker)
  if marker == "`" then
    return true
  end

  local before = text:sub(position - 1, position - 1)
  local after = text:sub(position + 1, position + 1)
  return not before:match("%s") and not after:match("[%w_]")
end

local function ascii_find_closing(text, marker, start)
  local position = start

  while true do
    position = text:find(marker, position, true)

    if position == nil then
      return nil
    end

    if not is_escaped(text, position)
      and ascii_delimiter_can_close(text, position, marker)
    then
      return position
    end

    position = position + 1
  end
end

local function ascii_inline_to_tex(text)
  local result = {}
  local plain = {}
  local position = 1

  local function flush_plain()
    if #plain > 0 then
      result[#result + 1] = ascii_escape_text(table.concat(plain))
      plain = {}
    end
  end

  while position <= #text do
    local character = text:sub(position, position)

    if character == "\\" then
      local following = text:sub(position + 1, position + 1)
      local name = text:sub(position + 1):match("^([%a@_:][%w@_:]*)")

      if name ~= nil then
        flush_plain()
        result[#result + 1] = "\\cs{" .. name .. "}"
        position = position + #name + 1
      elseif following == "`" or following == "*" or following == "_" then
        flush_plain()
        if following == "_" then
          result[#result + 1] = "\\_"
        elseif following == "`" then
          result[#result + 1] = "\\textasciigrave{}"
        else
          result[#result + 1] = following
        end
        position = position + 2
      else
        flush_plain()
        result[#result + 1] = "\\textbackslash{}"
        position = position + 1
      end

    elseif (character == "`" or character == "*" or character == "_")
      and ascii_delimiter_can_open(text, position, character)
    then
      local closing = ascii_find_closing(text, character, position + 1)

      if closing ~= nil and closing > position + 1 then
        local contents = text:sub(position + 1, closing - 1)
        flush_plain()

        if character == "`" then
          local cs_name = contents:match("^\\([%a@_:][%w@_:]*)$")

          if cs_name ~= nil then
            result[#result + 1] = "\\cs{" .. cs_name .. "}"
          else
            result[#result + 1] = "\\code{" .. ascii_escape_code(contents) .. "}"
          end
        elseif character == "*" then
          result[#result + 1] = "\\textbf{" .. ascii_inline_to_tex(contents) .. "}"
        else
          result[#result + 1] = "\\emph{" .. ascii_inline_to_tex(contents) .. "}"
        end

        position = closing + 1
      else
        plain[#plain + 1] = character
        position = position + 1
      end

    else
      plain[#plain + 1] = character
      position = position + 1
    end
  end

  flush_plain()
  return table.concat(result)
end

autodoc.ascii_inline_to_tex = ascii_inline_to_tex

% \ldeen{Temporäre Verbatim-Dateien}{Temporary verbatim files}
local function unique_temp_name()
  local name = string.format(
    "osgdoc-%s-%04d.tmp",
    tex.jobname,
    #verbtempfiles + 1
  )
  verbtempfiles[#verbtempfiles + 1] = name
  return name
end

% \ldeen{Hauptfunktion für AutoDocInput}{Main processing function}
function autodoc.autodoc_process(filename, guard, mode)
  local syntax = get_syntax(mode)
  local first = true
  local input = io.open(filename, "r")

  if not input then
    tex.error(
      "AutoDocInput: cannot open `" .. filename .. "'"
    )
    return
  end
%
% \ldeen{
%   Zum Parsen wird ein kleiner Zustandsautomat genutzt. Dabei haben die Zustände folgende Bedeutungen:
% }{
%   A small finite-state machine is used for parsing. The states have the following meanings:
% }
% 
% \begin{itemize}
%    \item \textbf{OUTSIDE}: 
%      \ldeen{
%        Außerhalb eines ausgewählten Guard-Bereichs; keine Verbatim-Datei offen.
%        }{
%          Outside of a selected guard area;  no verbatim file open}
%    \item \textbf{OUTSIDE\_VERBATIM}: 
%      \ldeen{
%        Außerhalb eines ausgewählten Guard-Bereichs; eine Verbatim-Datei ist
%        weiterhin offen. Der nächste passende Guard setzt denselben Codeblock
%      fort.
%      }{
%        Outside a selected guard region; a verbatim file is
%        still open. The next matching guard continues the same code block
%      }
%    \item \textbf{START}:
%      \ldeen{
%        Direkt hinter einem öffnenden Guard; noch keine Entscheidung zwischen
%        Dokumentation und Code getroffen.
%      }{
%        Immediately after an opening guard; no decision has yet been made between
%        documentation and code.
%      }
%    \item \textbf{VERBATIM}:
%      \ldeen{
%        Innerhalb eines ausgewählten Guard-Bereichs und mit geöffneter
%        Verbatim-Datei.
%      }{
%        Inside a selected guard region with opened verbatim file.
%      }
%
%    \item \textbf{COMMENT}:
%      \ldeen{
%        Innerhalb eines ausgewählten Guard-Bereichs und in Dokumentation.
%      }{
%        Within a selected guard area and in documentation.
%      }
%      \item \textbf{LONG\_COMMENT}:
%        \ldeen{
%          Innerhalb eines mehrzeiligen Lua-Dokumentationskommentars.
%        }{
%          Within a multi-line Lua documentation comment.
%        }
%  \end{itemize}
% 
  local OUTSIDE = 0
  local OUTSIDE_VERBATIM = 1
  local START = 2
  local VERBATIM = 3
  local COMMENT = 4
  local LONG_COMMENT = 5

  local state = OUTSIDE

  local long_comment = nil
  local long_comment_return_state = nil

  local verbatim_file = nil
  local verbatim_filename = nil

  local line_number = 0
  local documentation_list_open = false

  local function warn(message)
    tex.warning(
      string.format(
        "AutoDocInput: %s in %s:%d",
        message,
        filename,
        line_number
      )
    )
  end

  local function close_documentation_list()
    if documentation_list_open then
      tex.print("\\end{itemize}")
      documentation_list_open = false
    end
  end

  local function print_documentation(text)
    text = text or ""

    if syntax.documentation_format ~= "ascii" then
      tex.print(escape_cs_underscores(text))
      return
    end

    local heading_marks, heading = text:match("^%s*(##?)%s+(.+)%s*$")

    if heading ~= nil then
      close_documentation_list()
      local command = heading_marks == "#" and "section" or "subsection"
      tex.print(
        "\\" .. command .. "{"
          .. escape_cs_underscores(ascii_inline_to_tex(heading))
          .. "}"
      )
      return
    end

    local item = text:match("^%s*[%-%*]%s+(.+)$")

    if item ~= nil then
      if not documentation_list_open then
        tex.print("\\begin{itemize}")
        documentation_list_open = true
      end

      tex.print("\\item " .. escape_cs_underscores(ascii_inline_to_tex(item)))
      return
    end

    close_documentation_list()

    if text:match("^%s*===+%s*$") then
      tex.print("\\par\\smallskip\\noindent\\hrule\\smallskip")
    else
      tex.print(escape_cs_underscores(ascii_inline_to_tex(text)))
    end
  end

  local function open_verbatim(first_line)
    if verbatim_file ~= nil then
      tex.error(
        "AutoDocInput: internal error",
        {
          string.format(
            "Attempt to open a second verbatim file in %s:%d.",
            filename,
            line_number
          )
        }
      )
      return false
    end

    verbatim_filename = unique_temp_name()

    verbatim_file = io.open(verbatim_filename, "w")

    if not verbatim_file then
      tex.error(
        "AutoDocInput: cannot open temporary file",
        {verbatim_filename}
      )
      verbatim_filename = nil
      return false
    end

    verbatim_file:write(first_line, "\n")
    return true
  end

  local function write_verbatim(line)
    if verbatim_file == nil then
      tex.error(
        "AutoDocInput: internal parser error",
        {
          string.format(
            "VERBATIM state without an open file in %s:%d.",
            filename,
            line_number
          )
        }
      )
      return false
    end

    verbatim_file:write(line, "\n")
    return true
  end

  local function close_verbatim()
    if verbatim_file == nil then
      return
    end

    verbatim_file:close()
    verbatim_file = nil

% \ldeen{
%   Die Zeilennummer entspricht der logischen Zeilennummer der Implementation, d.\,h. generierter Boilerplate wird
%   nicht berücksichtigt.
% }{
%   The line number corresponds to the logical line number in the implementation; that is, generated boilerplate is
%   not taken into account.
% } 
    if first then 
      tex.print(
        "\\VerbatimInput[firstnumber=1]{"
          .. verbatim_filename
          .. "}"
      )
      first = false
    else  
      tex.print(
        "\\VerbatimInput[firstnumber=last]{"
          .. verbatim_filename
          .. "}"
      )
    end
    verbatim_filename = nil
  end

  local function begin_long_comment(start, return_state)
    long_comment = start
    long_comment_return_state = return_state
    state = LONG_COMMENT

    if start.text == "" then
      return
    end

    local text, finished =
      lua_long_comment_line(start.text, long_comment)

    print_documentation(text)

    if finished then
      state = long_comment_return_state
      long_comment_return_state = nil
      long_comment = nil
    end
  end

  for line in input:lines() do
% \ldeen{Der eigentliche Automat}{The actual state machine}
    line_number = line_number + 1

    if state == LONG_COMMENT then
      local text, finished =
        lua_long_comment_line(line, long_comment)

      print_documentation(text)

      if finished then
        state = long_comment_return_state
        long_comment_return_state = nil
        long_comment = nil
      end

    else
      local opening =
        is_opening_guard(line, syntax, guard)

      local closing =
        is_closing_guard(line, syntax, guard)

      local documentation =
        comment_text(line, syntax)

      local long_start = nil

      if syntax.comment_kind == "lua" then
        long_start = lua_long_comment_start(line)
      end

      if state == OUTSIDE then
        if opening then
          state = START
        end

      elseif state == OUTSIDE_VERBATIM then
        if opening then
          if verbatim_file == nil then
            tex.error(
              "AutoDocInput: internal parser error",
              {
                string.format(
                  "OUTSIDE_VERBATIM without an open file in %s:%d.",
                  filename,
                  line_number
                )
              }
            )
            state = START
          else
            state = VERBATIM
          end
        end

      elseif state == START then
        if closing then
          close_documentation_list()
          -- Leerer Guard-Bereich; es wurde keine Datei geöffnet.
          state = OUTSIDE

        elseif opening then
          warn("cascading opening guards")

        elseif long_start then
          begin_long_comment(long_start, COMMENT)

        elseif documentation ~= nil then
          print_documentation(documentation)
          state = COMMENT

        else
          close_documentation_list()
          if open_verbatim(line) then
            state = VERBATIM
          else
            state = OUTSIDE
          end
        end

      elseif state == VERBATIM then
        if closing then
          state = OUTSIDE_VERBATIM

        elseif opening then
          warn("cascading opening guards")

        elseif long_start then
          close_verbatim()
          begin_long_comment(long_start, COMMENT)

        elseif documentation ~= nil then
          close_verbatim()
          print_documentation(documentation)
          state = COMMENT

        else
          write_verbatim(line)
        end

      elseif state == COMMENT then
        if closing then
          close_documentation_list()
          state = OUTSIDE

        elseif opening then
          warn("cascading opening guards")

        elseif long_start then
          begin_long_comment(long_start, COMMENT)

        elseif documentation ~= nil then
          print_documentation(documentation)

        else
          close_documentation_list()
          if open_verbatim(line) then
            state = VERBATIM
          else
            state = OUTSIDE
          end
        end

      else
        tex.error(
          "AutoDocInput: invalid parser state",
          {
            string.format(
              "Invalid state %s in %s:%d.",
              tostring(state),
              filename,
              line_number
            )
          }
        )
        break
      end
    end
  end

  if state == LONG_COMMENT then
    warn("unterminated Lua long comment")
  end

  close_documentation_list()

  -- A file can be open in both, VERBATIM or OUTSIDE_VERBATIM
  if verbatim_file ~= nil then
    close_verbatim()
  end

  input:close()
end

% \ldeen{Temporäre Dateien aufräumen.}{Cleanup temporary files.}
luatexbase.add_to_callback(
  "stop_run",
  function()
    for _, filename in ipairs(verbtempfiles) do
      os.remove(filename)
    end
  end,
  "cleanup osgdoc verbatim temporary files"
)
\end{luacode*}


% \subsection*{\ldeen{Dateiinformationen}{File Information}}
% \ldeen*{
% @1 und @2 nutzen beide die Lua-Funktion @3.
% }{
% Both, @1 and @2 use the lua function @3.
% }{\cs{UseVersionOf}}{\cs{UseDateOf}}{\code{useversion}}
\ifosgdoc@readprov\else
  \NewExpandableDocumentCommand{\UseVersionOf}{m}{%
    \directlua{fileinfo.useversion(\luastring{#1})}%
  }

  \NewExpandableDocumentCommand{\UseDateOf}{m}{%
    \directlua{fileinfo.usedate(\luastring{#1})}%
  }
\fi

\begin{luacode*}
fileinfo = fileinfo or {}

fileinfo.data = {}

function fileinfo.getinfo(filename)
   local cached = fileinfo.data[filename]
   if cached ~= nil then
      return cached
   end

   local resolved_filename = kpse.find_file(filename)
   local f = texassert(
      resolved_filename and io.open(resolved_filename, "r"),
      "Can't read '" .. filename .. "'"
   )
   local s = f:read("*a")
   f:close()
   local ftype = nil
   local name,date,version,descr = s:match(
      "\\ProvidesExplPackage%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}")
   ftype = "package"
   if name == nil then
      name,date,descr = s:match(
	 "\\ProvidesPackage{([^}]*)}%s*%[(%d%d%d%d[%-/]%d%d[%-/]%d%d)%s?([^%]]*)%]")
      if name ~= nil then
	 descr = descr or "??"
	 version = descr:match("^v?(%d+[%p%d]*%l*)%s")
      end
   end
   
   if name == nil then
      name,date,version,descr = s:match(
	 "\\ProvidesExplClass%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}")
      ftype = "class"
      if name == nil then
	 name,date,descr = s:match(
	    "\\ProvidesClass{([^}]*)}%s*%[(%d%d%d%d[%-/]%d%d[%-/]%d%d)%s?([^%]]*)%]")
	 if name ~= nil then
	    descr = descr or "??"
	    version = descr:match("^v?(%d+(%p%d+)*%l*)%s")
	 end
      end
   end
   if name == nil then
      name,date,version,descr = s:match(
	 "\\ProvidesExplFile%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}%s*{([^}]*)}")
      ftype = "file"
      if name == nil then
	 name,date,descr = s:match(
	    "\\ProvidesFile{([^}]*)}%s*%[(%d%d%d%d[%-/]%d%d[%-/]%d%d)%s?([^%]]*)%]")
	 if name ~= nil then
	    descr = descr or "??"
	    version = descr:match("^v?(%d+(%p%d+)*%l*)%s")
	 end
      end
   end
   if name ~= nil then
      date = date or "??"
      version = version or "??"
      cached = {
         name = name,
         date = date,
         version = version,
         descr = descr,
         type = ftype,
      }
      fileinfo.data[filename] = cached
   end

   return cached
end

function fileinfo.useversion(filename)
   local info = fileinfo.getinfo(filename)
   tex.sprint(info and info.version or "??")
end

function fileinfo.usedate(filename)
   local info = fileinfo.getinfo(filename)
   tex.sprint(info and info.date or "??")
end
\end{luacode*}

%</pkg>
