%%
%% This is file `osgdoc.sty',
%% generated with the docstrip utility.
%%
%% The original source files were:
%%
%% osgdoc.dtx  (with options: `pkg')
%% 
%% ========================================================================
%%  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
%% ========================================================================
%% 
\ProvidesExplPackage
{osgdoc}
{2026-06-11}
{0.0.7}
{small tools for documentation}
\sys_if_engine_luatex:TF{
  \def\error{}
  \RequirePackage{luacode}
}{
  \def\error{\errmessage{LuaLaTeX~is~required~to~use~this~package.}\endinput}
}
\error
\RequirePackage{luacode}
\RequirePackage{fancyvrb}
\fvset{formatcom=\color{blue},numbers=left}

\begin{luacode*}
  function texassert(v, msg)
    if v then
      return v
    end
    tex.error("Error", {msg or "assertion failed"})
  end
\end{luacode*}
\NewDocumentCommand{\AutoDocInput}{O{tex} m m}{
  \directlua{
    autodoc.autodoc_process(
      \luastring{#2},
      \luastring{#3},
      \luastring{#1}
    )
  }
}

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

local verbtempfiles = {}
local syntaxes = {
  ["tex"] = {
    guard_prefix = "%%",
    comment_kind = "tex",
  },

  ["lua"] = {
    guard_prefix = "%-%-",
    comment_kind = "lua",
  },

  ["tex/lua"] = {
    guard_prefix = "%%",
    comment_kind = "lua",
  },
}

local function get_syntax(mode)
  mode = mode or "tex"

  local syntax = syntaxes[mode]

  if syntax then
    return syntax
  end

  tex.error(
    "AutoDocInput: unknown syntax mode `" .. tostring(mode) .. "'",
    {
      "Supported syntax modes are:",
      "  tex",
      "  lua",
      "  tex/lua",
    }
  )

  return syntaxes.tex
end

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

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

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

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

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

  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


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

local function unique_temp_name()
  local name = string.format(
    "osgdoc-%s-%04d.tmp",
    tex.jobname,
    #verbtempfiles + 1
  )
  verbtempfiles[#verbtempfiles + 1] = name
  return name
end

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
  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 function warn(message)
    tex.warning(
      string.format(
        "AutoDocInput: %s in %s:%d",
        message,
        filename,
        line_number
      )
    )
  end

  local function print_documentation(text)
    tex.print(text or "")
  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

    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
    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
          -- Leerer Guard-Bereich; es wurde keine Datei ge^^c3^^b6ffnet.
          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
          if open_verbatim(line) then
            state = VERBATIM
          else
            state = OUTSIDE
          end
        end

      elseif state == VERBATIM then
        if closing then
          state = OUTSIDE_VERBATIM
          -- Don't close file here! A later contiuation is possible.

        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
          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
          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

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

  input:close()
end

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*}

\NewDocumentCommand{\UseVersionOf}{m}
{
   \directlua{fileinfo.useversion(\luastring{#1})}
}

\NewDocumentCommand{\UseDateOf}{m}
{
   \directlua{fileinfo.usedate(\luastring{#1})}
}

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

fileinfo.data = {}

function fileinfo.getinfo(filename)
   filename = kpse.find_file(filename)
   local f = texassert(io.open(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 "??"
      fileinfo.data["name"] = name
      fileinfo.data["date"] = date
      fileinfo.data["version"] = version
      fileinfo.data["descr"] = descr
      fileinfo.data["type"] = ftype
   end
end

function fileinfo.useversion(filename)
   if fileinfo.data["name"] == nil then
      fileinfo.getinfo(filename)
   end
   local res = fileinfo.data["version"]
   if res == nil then
      tex.print("??")
   else
      tex.print(res)
   end
end

function fileinfo.usedate(filename)
   if fileinfo.data["name"] == nil then
      fileinfo.getinfo(filename)
   end
   local res = fileinfo.data["date"]
   if res == nil then
      tex.print("??")
   else
      tex.print(res)
   end
end
\end{luacode*}
%% 
%%  This work is "maintained" (as per LPPL maintenance status) by
%%  Matthias Werner.
%%
%% End of file `osgdoc.sty'.

