Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  Magic.gi   Sprache: unbekannt

 
# AutoDoc: Generate documentation from GAP source code
#
# Copyright of AutoDoc belongs to its developers.
# Please refer to the COPYRIGHT file for details.
#
# SPDX-License-Identifier: GPL-2.0-or-later

# Given a string containing a ".", , return its suffix,
# i.e. the bit after the last ".". For example, given "test.txt",
# it returns "txt".
BindGlobal( "AUTODOC_GetSuffix",
function(str)
    local i;
    i := Length(str);
    while i > 0 and str[i] <> '.' do i := i - 1; od;
    if i = 0 then return ""; fi;
    return str{[i+1..Length(str)]};
end );

# Scan the given (by name) subdirs of a package dir for
# files with one of the given extensions, and return the corresponding
# filenames, as relative paths (relative to the package dir).
#
# For example, the invocation
#   AUTODOC_FindMatchingFiles(pkgdir, [ "gap/" ], [ "gi", "gd" ]);
# might return a list looking like
#  [ "gap/AutoDocMainFunction.gd", "gap/AutoDocMainFunction.gi", ... ]
BindGlobal( "AUTODOC_FindMatchingFiles",
function (pkgdir, subdirs, extensions)
    local d_rel, d, tmp, files, result;

    result := [];

    for d_rel in subdirs do
        # Get the absolute path to the directory in side the package...
        d := Filename( pkgdir, d_rel );
        if not IsDirectoryPath( d ) then
            continue;
        fi;
        d := Directory( d );
        # ... but also keep the relative path (such as "gap")
        if d_rel = "" or d_rel = "." then
            d_rel := "";
        else
            d_rel := Directory( d_rel );
        fi;

        files := DirectoryContents( d );
        Sort( files );
        for tmp in files do
            if not AUTODOC_GetSuffix( tmp ) in extensions then
                continue;
            fi;
            if not IsReadableFile( Filename( d, tmp ) ) then
                continue;
            fi;
            if d_rel = "" then
                Add( result, tmp );
            else
                Add( result, Filename( d_rel, tmp ) );
            fi;
        od;
    od;
    return result;
end );

#
InstallGlobalFunction( AutoDoc,
function( arg )
    local pkgname, pkginfo, pkgdir,
          opt, scaffold, gapdoc, extract_examples, autodoc, i,
          doc_dir, doc_dir_rel, tmp, key, val, file,
          pkgdirstr, docdirstr,
          title_page, tree, is_worksheet,
          position_document_class,
          args;

    if Length( arg ) >= 3 then
        Error( "too many arguments" );
    fi;

    # check whether the last argument is an options record
    if Length( arg ) > 0 and IsRecord( arg[Length(arg)] ) then
        opt := Remove( arg );
    else
        opt := rec();
    fi;

    # check the first argument
    if Length(arg) = 0 then
        pkgdir := DirectoryCurrent( );
    elif IsString( arg[1] ) then
        pkgname := Remove( arg, 1 );
    elif IsDirectory( arg[1] ) then
        pkgdir := Remove( arg, 1 );
    fi;

    # if there are any arguments left, at least one was of unsupported type
    if Length(arg) > 0 then
        Error( "wrong arguments" );
    fi;

    if IsBound( pkgdir ) then
        is_worksheet := false;
        file := Filename( pkgdir, "PackageInfo.g" );
        if not IsExistingFile( file ) then
            Error( "no package name given and no PackageInfo.g file found" );
        elif not IsReadableFile( file ) then
            Error( "cannot read PackageInfo.g" );
        fi;
        Unbind( GAPInfo.PackageInfoCurrent );
        Read( file );
        if not IsBound( GAPInfo.PackageInfoCurrent ) then
            Error( "reading PackageInfo.g failed" );
        fi;
        pkginfo := GAPInfo.PackageInfoCurrent;
        if IsRecord( pkginfo.PackageDoc ) then
            pkginfo.PackageDoc:= [ pkginfo.PackageDoc ];
        fi;
        pkgname := pkginfo.PackageName;

    elif pkgname = "AutoDocWorksheet" then
        # For internal use only -- for details, refer to the AutoDocWorksheet() function.
        is_worksheet := true;
        pkginfo := rec( );
        pkgdir := DirectoryCurrent( );

    else
        is_worksheet := false;
        pkginfo := PackageInfo( pkgname );
        if IsEmpty( pkginfo ) then
            Error( "Could not find package ", pkgname );
        elif Length( pkginfo ) > 1 then
            Info( InfoWarning, 1, "multiple versions of package ", pkgname, " are present, using the first one" );
        fi;
        pkginfo := pkginfo[ 1 ];
        pkgdir := Directory( pkginfo.InstallationPath );
    fi;

    #
    # Check for user supplied options. If present, they take
    # precedence over any defaults as well as the opt record.
    #
    for key in [ "dir", "scaffold", "autodoc", "gapdoc", "extract_examples" ] do
        val := ValueOption( key );
        if val <> fail then
            opt.(key) := val;
        fi;
    od;

    #
    # Setup the output directory
    #
    if not IsBound( opt.dir ) then
        doc_dir := "doc";
    elif IsString( opt.dir ) or IsDirectory( opt.dir ) then
        doc_dir := opt.dir;
    else
        Error( "opt.dir must be a string containing a path, or a directory object" );
    fi;

    if IsString( doc_dir ) then
        # Record the relative version of the path
        # FIXME: this assumes that doc_dir contains a relative path in the first place...
        doc_dir_rel := Directory( doc_dir );

        # We intentionally do not use
        #   DirectoriesPackageLibrary( pkgname, "doc" )
        # because it returns an empty list if the subdirectory is missing.
        # But we want to handle that case by creating the directory.
        doc_dir := Filename( pkgdir, doc_dir );
        doc_dir := Directory( doc_dir );

    else
        # In this case, if doc_dir happens to lie below pkgdir, we want the
        # doc_dir_rel to be the difference; if not we avoid binding doc_dir_rel
        # and leave MakeGAPDocDoc to muddle through with absolute paths.
        pkgdirstr := Filename( pkgdir, "" );
        docdirstr := Filename( doc_dir, "" );
        if StartsWith( docdirstr, pkgdirstr ) then
            doc_dir_rel :=
            Directory( docdirstr{[(Length(pkgdirstr)+1)..Length(docdirstr)]} );
        fi;
    fi;

    # Ensure the output directory exists, create it if necessary
    AUTODOC_CreateDirIfMissing(Filename(doc_dir, ""));

    # Let the developer know where we are generating the documentation.
    # This helps diagnose problems where multiple instances of a package
    # are visible to GAP and the wrong one is used for generating the
    # documentation.
    Info( InfoAutoDoc, 2, "Generating documentation in ", doc_dir);

    #
    # Extract scaffolding settings, which can be controlled via
    # opt.scaffold or pkginfo.AutoDoc. The former has precedence.
    #
    if not IsBound(opt.scaffold) then
        # Default: enable scaffolding if and only if pkginfo.AutoDoc is present
        if IsBound( pkginfo.AutoDoc ) then
            scaffold := rec( );
        fi;
    elif IsRecord(opt.scaffold) then
        scaffold := opt.scaffold;
    elif IsBool(opt.scaffold) then
        if opt.scaffold = true then
            scaffold := rec();
        fi;
    else
        Error("opt.scaffold must be a bool or a record");
    fi;

    # Merge pkginfo.AutoDoc into scaffold
    if IsBound(scaffold) and IsBound( pkginfo.AutoDoc ) then
        for key in RecNames( pkginfo.AutoDoc ) do
            if IsBound( scaffold.(key) ) then
                Info(InfoAutoDoc, 1, "WARNING: ", key, " specified in both PackageInfo.AutoDoc and opt.scaffold");
            else
                scaffold.(key) := pkginfo.AutoDoc.(key);
            fi;
        od;

    fi;

    if IsBound( scaffold ) then
        AUTODOC_SetIfMissing( scaffold, "TitlePage", rec() );
        AUTODOC_SetIfMissing( scaffold, "MainPage", true );
    fi;


    #
    # Extract AutoDoc settings
    #
    if not IsBound(opt.autodoc) and not is_worksheet then
        # Enable AutoDoc support if the package depends on AutoDoc.
        tmp := Concatenation( pkginfo.Dependencies.NeededOtherPackages,
                              pkginfo.Dependencies.SuggestedOtherPackages );
        ## Empty entries are allowed in Dependencies
        tmp := Filtered( tmp, i -> i <> [ ] );
        if ForAny( tmp, x -> LowercaseString(x[1]) = "autodoc" ) then
            autodoc := rec();
        fi;
    elif IsRecord(opt.autodoc) then
        autodoc := opt.autodoc;
    elif IsBool(opt.autodoc) and opt.autodoc = true then
        autodoc := rec();
    fi;

    if IsBound(autodoc) then
        if not IsBound( autodoc.files ) then
            autodoc.files := [ ];
        elif not IsList( autodoc.files ) then
            Error("autodoc.files must be a list");
        elif Length(autodoc.files) >0 and IsString( autodoc.files ) then
            Error("autodoc.files must be a list of strings, not a string");
        fi;

        if not is_worksheet then
            if not IsBound( autodoc.scan_dirs ) then
                autodoc.scan_dirs := [ ".", "gap", "lib", "examples", "examples/doc" ];
            fi;
            Append( autodoc.files, AUTODOC_FindMatchingFiles(pkgdir, autodoc.scan_dirs, [ "g", "gi", "gd", "autodoc" ]) );
            autodoc.files := DuplicateFreeList( autodoc.files );
        fi;

        # Make sure all of the files exist, making the file names absolute if
        # necessary
        for i in [ 1 .. Length( autodoc.files ) ] do
            if IsExistingFile( autodoc.files[ i ] ) then continue; fi;
            if IsExistingFile( Filename( pkgdir, autodoc.files[ i ] ) ) then
                autodoc.files[ i ] := Filename( pkgdir, autodoc.files[ i ] );
                continue;
            fi;
            Error( autodoc.files[ i ], " does not specify an existing file either as an absolute path or relative to the package directory" );
        od;

        if not IsBound( autodoc.level ) then
            autodoc.level := 0;
        fi;
    fi;

    #
    # Extract GAPDoc settings
    #
    if not IsBound( opt.gapdoc ) then
        # Enable GAPDoc support by default
        gapdoc := rec();
    elif IsRecord( opt.gapdoc ) then
        gapdoc := opt.gapdoc;
    elif IsBool( opt.gapdoc ) and opt.gapdoc = true then
        gapdoc := rec();
    fi;

    if IsBound( gapdoc ) then
        
        AUTODOC_SetIfMissing( gapdoc, "main", pkgname );

        if IsBound( pkginfo.PackageDoc ) and not IsEmpty( pkginfo.PackageDoc ) then
            if Length( pkginfo.PackageDoc ) > 1 then
                Info(InfoAutoDoc, 1, "WARNING: Package contains multiple books, only using the first one");
            fi;
            gapdoc.bookname := pkginfo.PackageDoc[1].BookName;
            gapdoc.SixFile := pkginfo.PackageDoc[1].SixFile;
        elif not is_worksheet then
            # Default: book name = package name
            gapdoc.bookname := pkgname;
            gapdoc.SixFile := "doc/manual.six";

            Info(InfoAutoDoc, 1, "WARNING: PackageInfo.g is missing a PackageDoc entry!");
            Info(InfoAutoDoc, 1, "Without this, your package manual will not be recognized by the GAP help system.");
            Info(InfoAutoDoc, 1, "You can correct this by adding the following to your PackageInfo.g:");
            Info(InfoAutoDoc, 1, "PackageDoc := rec(");
            Info(InfoAutoDoc, 1, "  BookName  := ~.PackageName,");
            Info(InfoAutoDoc, 1, "  ArchiveURLSubset := [\"doc\"],");
            Info(InfoAutoDoc, 1, "  HTMLStart := \"doc/chap0.html\",");
            Info(InfoAutoDoc, 1, "  PDFFile   := \"doc/manual.pdf\",");
            Info(InfoAutoDoc, 1, "  SixFile   := \"doc/manual.six\",");
            Info(InfoAutoDoc, 1, "  LongTitle := ~.Subtitle,");
            Info(InfoAutoDoc, 1, "),");
        fi;

        if not IsBound( gapdoc.files ) then
            gapdoc.files := [];
        elif not IsList( gapdoc.files ) then
            Error("gapdoc.files must be a list");
        elif not ForAll( gapdoc.files, IsString ) then
            Error("gapdoc.files must be a list of strings, not a string");
        fi;

        if not is_worksheet then
            if not IsBound( gapdoc.scan_dirs ) then
                gapdoc.scan_dirs := [ ".", "gap", "lib", "examples", "examples/doc" ];
            fi;
            Append( gapdoc.files, AUTODOC_FindMatchingFiles(pkgdir, gapdoc.scan_dirs, [ "g", "gi", "gd" ]) );
        fi;

        # Attempt to weed out duplicates as they may confuse GAPDoc (this
        # will not work if there are any non-normalized paths in the list).
        gapdoc.files := Set( gapdoc.files );

        # If possible, convert the file paths in gapdoc.files, which are
        # relative to the package directory, to paths which are relative to
        # the doc directory.

        if IsBound( doc_dir_rel ) then
            # For this, we assume that doc_dir_rel is normalized (e.g.
            # it does not contains '//') and relative.
            # FIXME: this is an ugly hack, can't we do something better?
            tmp := Number( Filename( doc_dir_rel, "" ), x -> x = '/' );
            tmp := Concatenation( ListWithIdenticalEntries(tmp, "../") );
            gapdoc.files := List( gapdoc.files, f -> Concatenation( tmp, f ) );
        else
            # Here presumably the doc_dir was given by an absolute path that
            # does not lie below the package dir. In that case, we can't make
            # the gapdoc.files relative to the doc dir, but rather we have no
            # choice but to make them absolute, which MakeGAPDocDoc can handle,
            # even if perhaps less gracefully/portably.
            gapdoc.files := List( gapdoc.files, f -> Filename( pkgdir, f ) );
        fi;
    fi;


    # read tree
    # FIXME: shouldn't tree be declared inside of an 'if IsBound(autodoc)' section?
    tree := DocumentationTree( );

    if IsBound( autodoc ) then
        if IsBound( autodoc.section_intros ) then
            AUTODOC_PROCESS_INTRO_STRINGS( autodoc.section_intros, tree );
        fi;

        AutoDocScanFiles( autodoc.files, pkgname, tree );
    fi;

    if is_worksheet then
        # FIXME: We use scaffold and autodoc here without checking whether
        # they are bound. Does that mean worksheets always use them?
        if IsBound( scaffold.TitlePage.Title ) then
            pkgname := scaffold.TitlePage.Title;

        elif IsBound( tree!.TitlePage.Title ) then
            pkgname := tree!.TitlePage.Title;

        elif IsBound( autodoc.files ) and Length( autodoc.files ) > 0  then
            tmp := autodoc.files[ 1 ];

            # Remove everything before the last '/'
            tmp := SplitString(tmp, "/");
            tmp := tmp[Length(tmp)];

            # Remove everything after the first '.'
            tmp := SplitString(tmp, ".");
            tmp := tmp[1];

            pkgname := tmp;

        else
            Error( "could not figure out a title." );
        fi;

        if not IsString( pkgname ) then
            pkgname := JoinStringsWithSeparator( pkgname, " " );
        fi;

        gapdoc.main := ReplacedString( pkgname, " ", "_" );
        gapdoc.bookname := ReplacedString( pkgname, " ", "_" );
    fi;

    #
    # Generate scaffold
    #
    if IsBound( scaffold ) then
        ## Syntax is [ "class", [ "options" ] ]
        if IsBound( scaffold.document_class ) then
            position_document_class := PositionSublist( GAPDoc2LaTeXProcs.Head, "documentclass" );

            if IsString( scaffold.document_class ) then
                scaffold.document_class := [ scaffold.document_class ];
            fi;

            if position_document_class = fail then
                Error( "something is wrong with the LaTeX header" );
            fi;

            GAPDoc2LaTeXProcs.Head := Concatenation(
                  GAPDoc2LaTeXProcs.Head{[ 1 .. PositionSublist( GAPDoc2LaTeXProcs.Head, "{", position_document_class ) ]},
                  scaffold.document_class[ 1 ],
                  GAPDoc2LaTeXProcs.Head{[ PositionSublist( GAPDoc2LaTeXProcs.Head, "}", position_document_class ) .. Length( GAPDoc2LaTeXProcs.Head ) ]} );

            if Length( scaffold.document_class ) = 2 then

                GAPDoc2LaTeXProcs.Head := Concatenation(
                      GAPDoc2LaTeXProcs.Head{[ 1 .. PositionSublist( GAPDoc2LaTeXProcs.Head, "[", position_document_class ) ]},
                      scaffold.document_class[ 2 ],
                      GAPDoc2LaTeXProcs.Head{[ PositionSublist( GAPDoc2LaTeXProcs.Head, "]", position_document_class ) .. Length( GAPDoc2LaTeXProcs.Head ) ]} );
            fi;
        fi;

        if IsBound( scaffold.latex_header_file ) then
            GAPDoc2LaTeXProcs.Head := StringFile( scaffold.latex_header_file );
        fi;

        AUTODOC_SetIfMissing( scaffold, "includes", [ ] );

        if IsBound( autodoc ) then
            # If scaffold.includes is already set, then we add
            # AutoDocMainFile.xml to it, but *only* if it not already
            # there. This way, package authors can control where
            # it is put in their includes list.
            if not _AUTODOC_GLOBAL_OPTION_RECORD.AutoDocMainFile in scaffold.includes then
                Add( scaffold.includes, _AUTODOC_GLOBAL_OPTION_RECORD.AutoDocMainFile );
            fi;
        fi;

        if IsBound( scaffold.bib ) and IsBool( scaffold.bib ) then
            if scaffold.bib = true then
                scaffold.bib := Concatenation( pkgname, ".bib" );
            else
                Unbind( scaffold.bib );
            fi;
        elif not IsBound( scaffold.bib ) then
            # If there is a doc/PKG.bib file, assume that we want to reference it in the scaffold.
            tmp := Concatenation( pkgname, ".bib" );
            if IsReadableFile( Filename( doc_dir, tmp ) ) then
                scaffold.bib := tmp;
            fi;
        fi;

        AUTODOC_SetIfMissing( scaffold, "index", true );

        if IsBound( gapdoc ) then
            if AUTODOC_GetSuffix( gapdoc.main ) = "xml" then
                scaffold.main_xml_file := gapdoc.main;
            else
                scaffold.main_xml_file := Concatenation( gapdoc.main, ".xml" );
            fi;
        fi;

        if IsBound( scaffold.TitlePage ) and scaffold.TitlePage <> false then
            title_page := ShallowCopy( scaffold.TitlePage );

            AUTODOC_MergeRecords( title_page, tree!.TitlePage );

            if not is_worksheet then
                AUTODOC_MergeRecords( title_page, ExtractTitleInfoFromPackageInfo( pkginfo ) );
            fi;

            # Worksheets get date as a list
            if is_worksheet then
                title_page!.Date := Concatenation( title_page!.Date );
            fi;

            CreateTitlePage( doc_dir, title_page );
        fi;

        CreateEntitiesPage( gapdoc.bookname, doc_dir, scaffold );

        if IsBound( scaffold.MainPage ) and scaffold.MainPage <> false then
            CreateMainPage( gapdoc.bookname, doc_dir, scaffold );
        fi;
    fi;

    #
    # Write AutoDoc XML files
    #
    if IsBound( autodoc ) then
        WriteDocumentation( tree, doc_dir, autodoc.level );
        if IsBound( gapdoc ) then
            if IsBound( doc_dir_rel ) then
                Add( gapdoc.files, "_Chunks.xml" );
            else
                Add( gapdoc.files, Filename( doc_dir, "_Chunks.xml" ) );
            fi;
        fi;
    fi;


    #
    # Run GAPDoc
    #
    if IsBound( gapdoc ) then

        AUTODOC_SetIfMissing(gapdoc, "LaTeXOptions", rec() );
        if not IsRecord( gapdoc.LaTeXOptions ) then
            Error("gapdoc.LaTeXOptions must be a record");
        fi;
        for key in RecNames( gapdoc.LaTeXOptions ) do
            if not IsString( gapdoc.LaTeXOptions.( key ) )
               and IsList( gapdoc.LaTeXOptions.( key ) )
               and LowercaseString( gapdoc.LaTeXOptions.( key )[ 1 ] ) = "file" then
                gapdoc.LaTeXOptions.( key ) := StringFile( gapdoc.LaTeXOptions.( key )[ 2 ] );
            fi;
        od;


        # Ask GAPDoc to use UTF-8 as input encoding for LaTeX, as the XML files
        # of the documentation are also in UTF-8 encoding, and may contain characters
        # not contained in the default Latin 1 encoding.
        AUTODOC_SetIfMissing( gapdoc.LaTeXOptions, "InputEncoding", "utf8" );
        SetGapDocLaTeXOptions( gapdoc.LaTeXOptions );
        
        ## HACK: If there is an empty index, MakeGAPDocDoc throws an error when creating the pdf.
        ## this addition prevents this by fake adding the index to the page number log. See issue 106.
        ## FIXME: Once an empty index is allowed in GapDoc, this should be removed.
        GAPDoc2LaTeXProcs.Tail := Concatenation(
            "\\immediate\\write\\pagenrlog{[\"Ind\", 0, 0], \\arabic{page},}\n",
            GAPDoc2LaTeXProcs.Tail );

        # Default parameters for MakeGAPDocDoc
        args := [ doc_dir, gapdoc.main, gapdoc.files, gapdoc.bookname, "MathJax" ];

        # Don't try to generate PDFs if pdflatex is not available
        if Filename( DirectoriesSystemPrograms(), "pdflatex" ) = fail then
            Add(args, "nopdf" );
        fi;

        # The global option "relativePath" can be set to ensure the manual
        # is built in such a way that all references to the GAP reference manual
        # are using relative file paths. This is mainly useful when building
        # a package manual for use in a distribution tarball.
        
        tmp := ValueOption( "relativePath" );
        
        if IsBound( gapdoc.gap_root_relative_path ) and tmp = fail then ## the option overrides the settings in the call.
            tmp := gapdoc.gap_root_relative_path;
        fi;
        
        if tmp = true then
            Add( args, "../../.." );
        elif IsString( tmp ) then
            Add( args, tmp );
        fi;

        # don't build PDF docs if the environment variable NOPDF is set
        if IsBound( GAPInfo.SystemEnvironment.NOPDF ) then
            Add( args, "nopdf" );
        fi;

        # Finally, invoke GAPDoc
        CallFuncList( MakeGAPDocDoc, args );

        # NOTE: We cannot just write CopyHTMLStyleFiles(doc_dir) here, as
        # CopyHTMLStyleFiles its argument directly to Directory(), leading
        # to an error in all GAP versions up to and including 4.8.6. This
        # will be fixed with GAP 4.9, where Directory() is made idempotent.
        CopyHTMLStyleFiles( Filename( doc_dir, "" ) );

        # The following (undocumented) API is there for compatibility
        # with old-style gapmacro.tex based package manuals. It
        # produces a manual.lab file which those packages can use if
        # they wish to link to things in the manual we are currently
        # generating. This can probably be removed eventually, but for
        # now, doing it does not hurt.

        # FIXME: It seems that this command does not work if pdflatex
        #        is not present. Maybe we should remove it.

        if IsBound( gapdoc.SixFile ) then
            file := Filename(pkgdir, gapdoc.SixFile);
            if file = fail or not IsReadableFile(file) then
                Error("could not open `", file, "' for package `", pkgname, "'.\n");
            fi;
            GAPDocManualLabFromSixFile( gapdoc.bookname, file );
        fi;

    fi;

    #
    # Handle extract_examples
    #

    if IsBound( opt.extract_examples ) then
        if IsRecord( opt.extract_examples ) then
            extract_examples := opt.extract_examples;
        elif opt.extract_examples = true then
            extract_examples := rec( );
        fi;
    fi;

    if IsBound( extract_examples ) then
        if is_worksheet then
            # HACK: not even sure this is really what we want for worksheets, but
            # it is useful for our "dogfood" test suite
            pkgdir := doc_dir;
        fi;
        if not IsBound( extract_examples.units ) then
            extract_examples.units := "Chapter";
        fi;
        if not IsBound( extract_examples.skip_empty_in_numbering ) then
            extract_examples.skip_empty_in_numbering := true;
        fi;
        AUTODOC_ExtractMyManualExamples( pkgname, pkgdir, doc_dir, gapdoc.main, gapdoc.files, extract_examples );
    fi;

    return true;
end );

[ Dauer der Verarbeitung: 0.39 Sekunden  (vorverarbeitet)  ]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge