%<*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 % %<*cls> \NeedsTeXFormat{LaTeX2e}[2022/06/01] \ProvidesExplClass {osgdoc} {2026-09-08} {v0.5.0} {A thin wrapper around cnltx-doc} \ExplSyntaxOff % %<*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{\%} \item[\option{lua}]: \code{--<*\meta{Guard}>}~/~\code{--} \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} % %<*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} % %<*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*} %