Building package documentation from source notebooks using the Workbench is as simple as a click of a button.

How can I automate the documentation build from the command line?

I believe that this requires some familiarity with Java build tools (ant?), which I haven't used before.


Alternatively, is it possible to build the documentation directly from Mathematica, without having to use Ant and docbuild.xml? The Workbench includes a DocumentationBuild package.


I want to do this to be able to automate all tasks necessary to build a ready-to-distribute Mathematica package.

  • 2
    My answer here could be a starting point, I guess. You just need to run Ant outside of WB. If I have the time, I may add to this later. – Leonid Shifrin Nov 14 '16 at 14:33
up vote 11 down vote accepted

Alternatively, is it possible to build the documentation directly from Mathematica, without having to use Ant and docbuild.xml? The Workbench includes a DocumentationBuild package.

Yes this is possible, but it requires tracing what docbuild.xml is doing. It's deleting build directory at beginning, and copying PacletInfo.m and everything, except */Guides/**, */ReferencePages/**, and */Tutorials/** files, from package directory to build directory at the end. Besides above trivial tasks it calls DocumentationBuild/SystemFiles/ant/Build/notebook.xml file which builds documentation notebooks, and /DocumentationBuild/SystemFiles/ant/Build/html.xml file which builds HTML version of documentation.

notebook.xml and html.xml contain over five hundred lines with most of it being blocks of Mathematica code. Fortunately most of this code is related to logging and exchanging data with Ant and parts are only related with building built-in Mathematica documentation, not user created packages. After trowing out those irrelevant parts following code can be assembled.

We start with some helper functions, mostly file-system related:

throwOnFailed // ClearAll
throwOnFailed[tag_]@$Failed := Throw[$Failed, tag]
throwOnFailed[tag_]@expr_ := expr

(* OptionValue reports OptionValue::nodef warning when options,
   from which value is taken, contain options not listed in used defaults.
   We could pass all relevant defaults as first argument of OptionValue,
   or in OptionsPattern, but doc-building functions usually pass options
   to called functions and defaults (or symbols on which they are set)
   are scattered all around, so it's easier to just supress this warrning
   and allow passing of arbitrary options. *)
nodefQuiet = Function[Null, Quiet[#, OptionValue::nodef], HoldFirst];

cleanDir // ClearAll
cleanDir::unexpectedFiles = "\
Directory `1` exists and contains files `2` that don't match expected \
patterns `3`. Directory was not cleaned.";
cleanDir::file = "\
File `1` already exists, but is not a directory. \
Clean directory not created.";
cleanDir[dirName_String?DirectoryQ, All | "*" | Verbatim@__] := (
    DeleteDirectory[dirName, DeleteContents -> True];
    CreateDirectory[dirName]
)
cleanDir[dirName_String?DirectoryQ, deletableFileNames_] :=
    With[
        {
            undeletable = Select[
                Complement[
                    FileNames[All, dirName, Infinity],
                    FileNames[deletableFileNames, dirName, Infinity]
                ],
                Not@*DirectoryQ
            ]
        },
        If[undeletable === {},
            DeleteDirectory[dirName, DeleteContents -> True];
            CreateDirectory[dirName]
        (* else *),
            Message[cleanDir::unexpectedFiles,
                HoldForm@dirName,
                HoldForm@undeletable,
                HoldForm@deletableFileNames
            ];
            $Failed
        ]
    ]
cleanDir[dirName_String?FileExistsQ, _] := (
    Message[cleanDir::file, HoldForm@dirName];
    $Failed
)
cleanDir[dirName_String, _] := CreateDirectory[dirName]
cleanDir[dirNames : {___String}, deletableFileNames_] :=
    cleanDir[#, deletableFileNames]& /@ dirNames

ensureDir // ClearAll
ensureDir[dir_String] := (
    Quiet[DeleteFile@dir, {DeleteFile::fdir, DeleteFile::fdnfnd}];
    Quiet[CreateDirectory@dir, CreateDirectory::filex]
)

forcedCopyFile // ClearAll
forcedCopyFile[from_String, to_String] := (
    Quiet[DeleteDirectory[to, DeleteContents -> True], DeleteDirectory::nodir];
    Quiet[CreateDirectory@DirectoryName@to, CreateDirectory::filex];
    CopyFile[from, to, OverwriteTarget -> True]
)

copyFiles // ClearAll
copyFiles[
    from_String, to_String, forms_ : All, n_ : Infinity,
    OptionsPattern@FileNames
] :=
    With[{fromDepth = FileNameDepth@from},
        Cases[FileNames[forms, from, n, IgnoreCase -> OptionValue@IgnoreCase],
            f_ /; FileType@f === File :>
                forcedCopyFile[f,
                    FileNameJoin@{to, FileNameDrop[f, fromDepth]}
                ]
        ]
    ]

withDirectory // ClearAll
withDirectory[Inherited] = Identity;
withDirectory[dir_String?DirectoryQ] := Function[Null,
    With[{oldCWD = Directory[]},
        CheckAll[
            SetDirectory@dir;
            #
            ,
            (
                SetDirectory@oldCWD;
                ReleaseHold@#2;
                #1
            )&
        ]
    ],
    HoldFirst
];

(* Documentation building leaves bunch of open links,
   this environment will close all links created during
   evaluation of its body. *)
linkBlock = Function[Null,
    With[{oldLinks = Links[]},
        CheckAll[#,
            (
                LinkClose /@ Complement[Links[],
                    oldLinks, {$ParentLink, $CurrentLink}
                ];
                ReleaseHold@#2;
                #1
            )&
        ]
    ],
    HoldFirst
];

Now functions assembled from code from notebook.xml and html.xml files, together with some helper functions solving common problems like customization of web URLs or switching off cell rasterization. We start by loading DocumentationBuild` package, path to Eclipse module containing it should be adapted to your installation.

If[Not@MemberQ[$Path, #], PrependTo[$Path, #]]&@
   "path/to/eclipse/configuration/org.eclipse.osgi/679/0/.cp/MathematicaSource";
Needs@"DocumentationBuild`"

$languages = <|
    "English" -> "en",
    "Japanese" -> "ja",
    "ChineseSimplified" -> "zh",
    "Spanish" -> "es"
|>;

validLangQ = KeyExistsQ[$languages, #]&;

$notebookInformationMetaDataNames = <|
    "Title" -> "windowtitle",
    "TitleModifier" -> "titlemodifier",
    "HistoryData" -> "history",
    "EntityType" -> "type",
    "PacletName" -> "paclet",
    "Context" -> "context",
    "URI" -> "uri",
    "Keywords" -> "keywords",
    "Underdevelopment" -> None,
    "TutorialCollectionLinks" -> "tutorialcollectionlinks",
    "ForeignFunctionKeywords" -> None,
    "Synonyms" -> "synonyms",
    "Summary" -> "summary",
    "Flag" -> "status",
    "IndexQ" -> "index"
|>;

metaDataToNotebookInformation::missingInfo = "\
Key `1` not present in MetaData: `2`";
metaDataToNotebookInformation =
    With[
        {
            info =
                Replace@Dispatch@Join[#, {
                    None -> None,
                    k_ :> (
                        Message[metaDataToNotebookInformation::missingInfo,
                            HoldForm@k, HoldForm@#
                        ];
                        Return[$Failed, With]
                    )
                }] /@
                    $notebookInformationMetaDataNames
        },
        Normal@ReplacePart[info, {
            "HistoryData" -> ({
                "New" -> #1, "Modified" -> #2,
                "Obsolete" -> #3, "Excised" -> #4
            }& @@ info@"HistoryData"),
            "Underdevelopment" -> False,
            "ForeignFunctionKeywords" -> {},
            "Flag" -> Replace[info@"Flag", "None" -> None]
        }]
    ]&;

fullSourceInformation // ClearAll
fullSourceInformation[
    inputDir_String, lang:(_String?validLangQ):"English", appName_String
] :=
    Module[{builtinSrcInfo, packageSrcInfo},
        builtinSrcInfo = Get@FileNameJoin@{
            $DocumentationBuildDirectory, "Internal", "data",
            "SourceInformation." <> Replace[lang, $languages] <> ".m"
        };
        If[builtinSrcInfo === $Failed, Return[$Failed, Module]];
        packageSrcInfo = DocumentationBuild`Info`CreateSourceInformation[
            inputDir, lang, appName
        ];
        Join[FilterRules[builtinSrcInfo, Except@packageSrcInfo], packageSrcInfo]
    ]

originalDotHTML // ClearAll
originalDotHTML // DownValues =
    DownValues@DotHTML /. DotHTML -> originalDotHTML;

withFixedHTMLExportLinks // ClearAll
withFixedHTMLExportLinks@appName_String := Function[Null,
    Internal`InheritedBlock[{DotHTML},
        DotHTML[s_String] := originalDotHTML@StringReplace[s,
            StartOfString ~~ "http://reference.wolfram.com" | ""
                ~~ "/language/" <> appName <> "/" ~~ rest___ :>
                    "../" <> rest
            ];
        #
    ],
    HoldFirst
]

withNoGraphicsToBitmapsConversion = Function[Null,
    Internal`InheritedBlock[{ApplyConvertGraphicsToBitmapsToNotebook},
        ApplyConvertGraphicsToBitmapsToNotebook = #1&;
        #
    ],
    HoldFirst
];

withCustomWebURL // ClearAll
withCustomWebURL@None := Function[Null,
    Internal`InheritedBlock[{DocumentationBuild`Make`Private`MakeAnchorBarCell},
        DocumentationBuild`Make`Private`MakeAnchorBarCell =
            Internal`InheritedBlock[
                {
                    DocumentationBuild`Make`Private`MakeAnchorBarCell,
                    StringMatchQ
                }
                ,
                DocumentationBuild`Make`Private`MakeAnchorBarCell =.;
                Unprotect@StringMatchQ;
                (* This StringMatchQ test determines
                   whether web links are added.  *)
                StringMatchQ[_, "*ref/message/*"] = True;

                DocumentationBuild`Make`Private`MakeAnchorBarCell@##
            ]&;
        #
    ],
    HoldFirst
]
withCustomWebURL@webURL_String := Function[Null,
    Internal`InheritedBlock[{DocumentationBuild`Make`Private`MakeAnchorBarCell},
        DownValues@DocumentationBuild`Make`Private`MakeAnchorBarCell =
            DownValues@DocumentationBuild`Make`Private`MakeAnchorBarCell /.
                "http://reference.wolfram.com/language/" -> webURL;
        #
    ],
    HoldFirst
]

withCustomNonSplittableCamelCase // ClearAll
withCustomNonSplittableCamelCase@Automatic = Identity;
withCustomNonSplittableCamelCase@strPatt_ := Function[Null,
    Internal`InheritedBlock[{AddSpaceToCamelCase},
        (* https://mathematica.stackexchange.com/a/132294/14303 *)
        DownValues@AddSpaceToCamelCase =
            DownValues@AddSpaceToCamelCase /.
                aut : HoldPattern[Alternatives][__String] :>
                    (strPatt /. Automatic -> aut);
        #
    ],
    HoldFirst
]

docBuildEnv // ClearAll
docBuildEnv // Options = {
    "CoreInformation" -> Automatic,
    "Debug" -> False,
    "LogFunction" -> Print,
    "FrontEndOptions" ->
        {RenderingOptions -> {"HardwareAntialiasingQuality" -> 1}},
    "CloseLinks" -> True
};
docBuildEnv[
    appName_String, inputDir_String, lang:(_String?validLangQ):"English",
    OptionsPattern[]
] := Function[Null,
    If[TrueQ@OptionValue@"CloseLinks", linkBlock, Identity]@Internal`InheritedBlock[
        {
            Print, Global`AntLog, DocumentationBuild`Info`Private`LogIt,
            DocumentationBuild`Make`Private`LogIt, DocumentationBuild`Utils`Private`LogIt
            ,
            $DocumentationExportDebug, $DocumentationBuildDebug,
            DocumentationBuild`Transform`debug,
            DocumentationBuild`Make`Private`$DocumentationExportDebug
            ,
            $CoreInformation, $Pages, $Graph, $NameToNumberRules,
            $NumberToNameRules, DocumentationBuild`indexer,
            URIsWithoutCloudSupport, URIsWithoutDesktopSupport,
            URIsWithoutPublicCloudSupport,
            $ShowStringCharacters, $XMLTransformPath
            ,
            DocumentationBuild`Utils`Private`appear, DocumentationBuild`Utils`Private`bb,
            DocumentationBuild`Info`Private`links, DocumentationBuild`Info`Private`linktypes,
            DocumentationBuild`Make`Private`GetRootCaptionList, DocumentationBuild`Export`Private`modifiedNotesQ,
            DocumentationBuild`Export`Private`ModifyRawCharacterList,
            DocumentationBuild`Export`Private`ModifyRawCharacterListForSolutions, DocumentationBuild`Make`Private`nb,
            DocumentationBuild`Make`Private`newSections, DocumentationBuild`Common`Private`oldids,
            DocumentationBuild`Make`Private`PlaceTutorialCollectionGroup,
            DocumentationBuild`Make`Private`ShortFormTitle, DocumentationBuild`Info`Private`str,
            DocumentationBuild`Utils`Private`TextCharacterReplacementRules,
            DocumentationBuild`Make`Private`tutorialCollection, DocumentationBuild`Export`Private`$InputCount,
            DocumentationBuild`Export`Private`$InputQueue, DocumentationBuild`Utils`Private`$Localization,
            DocumentationBuild`Common`Private`$PubsFELink
            ,
            Transmogrify`Private`atomval, Transmogrify`Private`btiDefaultImageReturn,
            Transmogrify`ImportCache`Private`CacheData, Transmogrify`Private`dims,
            Transmogrify`Private`filelist, Transmogrify`Private`funcs, Transmogrify`Private`GetNodeFunction,
            Transmogrify`ImportCache`Private`impopts, Transmogrify`Private`OUTPUT, Transmogrify`Private`params,
            Transmogrify`Private`rdp, Transmogrify`ImportCache`Private`time, Transmogrify`Layout`Private`vals,
            Transmogrify`Internal`$AtomReturnValue, Transmogrify`Internal`$AutoRecurse,
            Transmogrify`Private`$GroupOpen, Transmogrify`ImportCache`$ImportCache,
            Transmogrify`Private`$LastRequestedTransform, Transmogrify`Private`$Parameters,
            Transmogrify`Private`$Rules, Transmogrify`Private`$XMLTransformList
        },
        (* DocumentationBuild subpackages use different global variables
           as debug flags, set them all to single value. *)
        $DocumentationBuildDebug = $DocumentationExportDebug =
            DocumentationBuild`Transform`debug =
            DocumentationBuild`Make`Private`$DocumentationExportDebug =
            OptionValue@"Debug";

        (* Doc-building functions use different mechanisms for logging,
           some use Print or AntLog directly.
           Some use BuildLog which logs depending on $DocumentationBuildDebug,
           and calls Print or AntLog depending on value of Global`$AntBuildQ.
           Some use private LogIt function which is not defined at all. *)
        Unprotect@Print;
        Print = Global`AntLog = OptionValue@"LogFunction";
        Protect@Print;
        DocumentationBuild`Info`Private`LogIt =
            DocumentationBuild`Make`Private`LogIt =
            DocumentationBuild`Utils`Private`LogIt =
            BuildLog;

        (* Some building/exporting functions use FrontEnd
           and need specific $FrontEnd options. *)
        PubsEvaluateWithFE@SetOptions[$FrontEnd, OptionValue@"FrontEndOptions"];

        (* By default Automatic value of "CoreInformation" results
           in builtin-only info being used. Here, for Automatic,
           we combine builtin info with package specific info.
           Although many functions accept "CoreInformation" option,
           they cache it, on first usage, in $CoreInformation[lang] global variable,
           and subsequent functions always use this catched value,
           regardless of possible different explicit "CoreInformation" option setting.
           So just set it here once, for whole environment.
           If different core informations are really needed inside environment
           manually clear or reset $CoreInformation. Make definition delayed,
           so that if, in e.g. incremental build, there's nothong to do,
           then huge list with built-in core information will not be loaded at all. *)
        $CoreInformation[lang] := $CoreInformation[lang] = LoadCoreInformation[
            Replace[OptionValue@"CoreInformation",
                Automatic :> fullSourceInformation[inputDir, lang, appName]
            ],
            lang
        ];

        $Pages := $Pages = ComputeLinkTrails@GetGuidesDirectory[inputDir, lang];

        #
    ],
    HoldFirst
]

Inspired by Gulp.js, file processing will be composed of small easily chainable functions accepting as argument, and returning as result, some form of unified representation of a file. As this unified file representation I've chosen an Association with elements corresponding to file path, some representation of contents and any additional required metadata. We start with general functions useful in this approach:

virtualFileDate = Quiet[Check[
    FileDate@#, DateObject@0, FileDate::fdnfnd
], FileDate::fdnfnd]&;

toFileObject // ClearAll
toFileObject[] = <|
    "Path" -> ExpandFileName@#,
    "ModTime" -> virtualFileDate@#,
    "History" -> {}
|>&;

renameTo // ClearAll
renameTo[newDir_String] := renameTo[FileNameJoin@{newDir, FileNameTake@#Path}&];
renameTo[pathFunc_] := Append[#,
    {"Path" -> ExpandFileName@pathFunc@#, "History" -> {#History, #Path}}
]&;

exportUsing // ClearAll
exportUsing::failed = "Exporting to file `1` failed.";
exportUsing[exportFunc_] := (
    ensureDir@DirectoryName@#Path;
    Module[{result = exportFunc@#},
        If[StringQ@result && ExpandFileName@result === #Path &&
            FileType@result === File && FileByteCount@result >= 1
        (* then *),
            Append[#, "ModTime" -> FileDate@result]
        (* else *),
            Message[exportUsing::failed, #Path];
            #
        ]
    ]
)&

functionalIf // ClearAll
functionalIf[condFunc_, tFunc_, fFunc_ : Identity] :=
    If[condFunc@#, tFunc@#, fFunc@#]&
functionalIf[condFunc_, tFunc_, fFunc_, uFunct_] :=
    If[condFunc@#, tFunc@#, fFunc@#, uFunct@#]&

ifNewerThan // ClearAll
ifNewerThan[refPathFunc_, tFunc_, fFunc_ : Identity] :=
    If[#ModTime > virtualFileDate@refPathFunc@#, tFunc@#, fFunc@#]&

Now wrappers for documentation building functions adapting them to above scheme:

getDocPath // ClearAll
getDocPath::notSubPath = "\
File `1` is not inside directory `2`. Using its full path as DocPath.";
getDocPath[langDocDir: _String | Automatic : Automatic] :=
    With[
        {
            docBaseDirSeq = Sequence @@ If[langDocDir === Automatic,
                {Longest@___, "Documentation", _?validLangQ}
            (* else *),
                FileNameSplit@ExpandFileName@langDocDir
            ],
            msg = If[langDocDir === Automatic,
                "containing Documentation"<> $PathnameSeparator <>
                "(" <> ToString[Alternatives @@ Keys@$languages] <>
                ") sub-directory"
            (* else *),
                ExpandFileName@langDocDir
            ]
        },
        Append[#,
            "DocPath" -> FileNameJoin@Replace[FileNameSplit@#Path, {
                {docBaseDirSeq, docDirPath___, fileName_} :>
                    {docDirPath, FileBaseName@fileName},
                {docDirPath___, fileName_} :> (
                    Message[getDocPath::notSubPath,
                        HoldForm@#Path, HoldForm@msg
                    ];
                    {docDirPath, FileBaseName@fileName}
                )
            }]
        ]&
    ]

getDocNotebook // ClearAll
getDocNotebook::notANotebook = "\
Last expression from `1` file is not a Notebook expression.";
getDocNotebook::invNbInfo = "\
Could not extract notebook information from notebook `1`";
getDocNotebook[isSource : True | False : True, opts : OptionsPattern[]] :=
    With[{optsSeq = Sequence @@ Flatten@{opts}},
        Module[{nb, info, meta = {}},
            nb = Quiet[Get@#Path, General::newl];
            If[Head@nb === Notebook,
                If[isSource,
                    info = GetNotebookInformation[nb, optsSeq]
                (* else *),
                    meta = GetSearchMetaDataList[nb, optsSeq];
                    info = metaDataToNotebookInformation@meta
                ];
                If[info === $Failed, Message[getDocNotebook::invNbInfo, #Path]]
            (* else *),
                Message[getDocNotebook::notANotebook, #Path];
                nb = info = $Failed
            ];
            Append[#, {
                "Notebook" -> nb,
                "NotebookInformation" -> info,
                "NotebookMetaData" -> meta
            }]
        ]&
    ]

buildDocNotebook // ClearAll
buildDocNotebook // Options = {
    "WebURL" -> None,
    "NonSplittableCamelCase" -> Automatic,
    "ConvertGraphicsToBitmaps" -> True,
    "ExcludedDocFlagsPatt" ->
        "ExcisedFlag" | "FutureFlag" | "InternalFlag" | "TemporaryFlag"
};
buildDocNotebook[opts : OptionsPattern[]] :=
    With[
        {
            optsSeq = Sequence @@ Flatten@{opts},
            exlFlags = nodefQuiet@OptionValue@"ExcludedDocFlagsPatt",
            webUrlWrapper = withCustomWebURL@nodefQuiet@OptionValue@"WebURL",
            camelWrapper = withCustomNonSplittableCamelCase@
                nodefQuiet@OptionValue@"NonSplittableCamelCase",
            bitmapsWrapper =
                If[nodefQuiet@OptionValue@"ConvertGraphicsToBitmaps",
                    Identity
                (* else *),
                    withNoGraphicsToBitmapsConversion
                ]
        },
        If[#NotebookInformation === $Failed,
            #
        (* else *),
            Append[#,
                If[MatchQ[OptionValue[#NotebookInformation, "Flag"], exlFlags],
                    "Notebook" -> None
                (* else *),
                    With[
                        {nb = bitmapsWrapper@camelWrapper@webUrlWrapper@
                            MakeNotebook[
                                #Notebook, #NotebookInformation,
                                "FilePath" -> #Path, optsSeq
                            ]
                        },
                        {
                            "Notebook" -> nb,
                            "NotebookMetaData" ->
                                GetSearchMetaDataList[nb, optsSeq]
                        }
                    ]
                ]
            ]
        ]&
    ]

Needs@"DocumentationSearch`"
indexDocNotebook // ClearAll
indexDocNotebook[indexer_DocumentationNotebookIndexer] := (
    If[Not@MatchQ[#Notebook, None | $Failed],
        AddDocumentationNotebook[
            indexer,
            Import[#Path, {"NB", "Plaintext"}],
            #NotebookMetaData,
            #Path
        ]
    ];
    #
)&

builtNBPath // ClearAll
builtNBPath@outputDir_String := FileNameJoin@{outputDir, #DocPath <> ".nb"}&

builtHTMLPath // ClearAll
builtHTMLPath@outputDir_String := FileNameJoin@{
    outputDir,
    FilePathToDocURI[$PathnameSeparator <> #DocPath, $PathnameSeparator] <>
        ".html"
}&

A basic build, that only builds documentation notebooks, might look as follows.

inputDir = "path/to/MyApplication/Documentation/English";
outputDir = "path/to/build/MyApplication/Documentation/English";
docBuildEnv["MyApplication", inputDir][
    cleanDir[outputDir, "*.nb"];
    FileNames["*.nb", inputDir, Infinity] // Map[
        toFileObject[] /*
        getDocPath[] /*
        getDocNotebook[] /*
        buildDocNotebook[] /*
        renameTo[builtNBPath@outputDir] /*
        exportUsing[Export[#Path, #Notebook]&] /*
        (Last@#History -> #Path &)
    ]
]

Incremental build, building notebooks, HTML pages, and search index, might look like this:

appName = "MyApplication";
inputDir = "path/to/MyApplication/Documentation/English";
buildDir = "path/to/build";

docBuildEnv[appName, inputDir]@Module[
    {nbOutputDir, indexDir, indexSpellDir, htmlOutputDir, indexer, result}
    ,
    nbOutputDir = FileNameJoin@{buildDir, appName, "Documentation", "English"};
    indexDir = FileNameJoin@{nbOutputDir, "Index"};
    indexSpellDir = FileNameJoin@{nbOutputDir, "SpellIndex"};
    htmlOutputDir = FileNameJoin@{buildDir, "HTML", "English"};

    cleanDir[{indexDir, indexSpellDir},
        {"_*.cfs", "segments_*", "segments.gen"}
    ];
    indexer =
        NewDocumentationNotebookIndexer[indexDir, "Language" -> "English"];

    result = FileNames["*.nb", inputDir, Infinity] // Map[
        toFileObject[] /*
        getDocPath[] /*
        ifNewerThan[builtNBPath@nbOutputDir,
            getDocNotebook[] /*
            buildDocNotebook[] /*
            renameTo[builtNBPath@nbOutputDir] /*
            exportUsing[Export[#Path, #Notebook]&]
        (* else *),
            renameTo[builtNBPath@nbOutputDir] /*
            getDocNotebook[False]
        ] /*
        indexDocNotebook[indexer] /*
        ifNewerThan[builtHTMLPath@htmlOutputDir,
            renameTo[builtHTMLPath@htmlOutputDir] /*
            exportUsing[
                withFixedHTMLExportLinks[appName]@ExportWebPage[
                    #Path, #Notebook, #NotebookInformation,
                    "CompleteHTMLQ" -> True
                ]&
            ]
        (* else *),
            renameTo[builtHTMLPath@htmlOutputDir]
        ] /*
        (Flatten@{#History, #Path}&)
    ];

    CloseDocumentationNotebookIndexer[indexer];
    CreateSpellIndex[indexDir, indexSpellDir];

    result
]

If needed, it should be easy to inject some additional processing between build steps.

Continued in second answer.

  • Seeing that you are familiar with this, could you see these questions in chat? I am not ready to post these on the main site yet, nor do I want full answers, just some pointers (if you have seen the same problems before). chat.stackexchange.com/rooms/48795/documentation-tools – Szabolcs Nov 19 '16 at 19:33
  • 2
    I must say, your answers on this site are truly invaluable. – QuantumDot Dec 5 '16 at 3:34

Putting together all pieces from first answer, for general, commonest case, gives final buildDocumentation function:

nonStrOptRule // ClearAll
nonStrOptRule // Attributes = HoldAll;
nonStrOptRule[msg_MessageName, optName_, onError_] :=
    wrong : "" | Except@_String :> (
        Message[msg, HoldForm@optName, HoldForm@wrong];
        onError
    )

useProcessDirectory // ClearAll
useProcessDirectory // Attributes = HoldFirst;
useProcessDirectory[_, appDir_, Automatic] := withDirectory@appDir
useProcessDirectory[_, _, procDir_ : (_String?DirectoryQ | Inherited)] :=
    withDirectory@procDir
useProcessDirectory[onError_, _, procDir_] := (
    Message[buildDocumentation::optAutInhDir,
        HoldForm@"ProcessDirectory", HoldForm@procDir
    ];
    onError
)

buildDocumentation // ClearAll
buildDocumentation::optStr = "\
Value of option `1` -> `2` should be a non-empty string.";
buildDocumentation::optAutStr = "\
Value of option `1` -> `2` should be Automatic, or a non-empty string.";
buildDocumentation::optAutInhDir = "\
Value of option `1` -> `2` should be Automatic, Inherited, \
or a non-empty string with valid path to a directory.";
buildDocumentation // Options = {
    "ApplicationName" -> Automatic,
    "SourceDirectory" -> "Documentation",
    "BuildDirectory" -> FileNameJoin@{"..", "build"},
    "NBOutputDirectory" -> Automatic,
    "HTMLOutputDirectory" -> Automatic,
    "ProcessDirectory" -> Automatic,
    "Incremental" -> True,
    "BuildIndex" -> True,
    "NBBuilder" -> Function[{nbOutputDir, fullOpts},
        getDocNotebook[fullOpts] /*
        buildDocNotebook[fullOpts] /*
        renameTo[builtNBPath@nbOutputDir] /*
        exportUsing[Export[#Path, #Notebook]&]
    ],
    "HTMLExporter" -> Function[{appName, htmlOutputDir, fullOpts},
        renameTo[builtHTMLPath@htmlOutputDir] /*
        exportUsing[withFixedHTMLExportLinks[appName]@ExportWebPage[
            #Path, #Notebook, #NotebookInformation, Sequence @@ fullOpts
        ]&]
    ],
    "CompleteHTMLQ" -> True,
    "WebAssets" :> {
        {
            FileNameJoin@{$DocumentationBuildDirectory,
                "Internal", "web", "html", "2014", "standard"
            },
            "",
            {
                "*.js", "*.json", "*.css", "*.png", "*.gif", "*.jpg",
                "*.svg", "*.eot", "*.otf", "*.ttf", "*.woff"
            }
        },
        {
            FileNameJoin@{$DocumentationBuildDirectory,
                "Internal", "web", "html", "2014", "minimal", "javascript"
            },
            "javascript",
            "faster-page-load.js"
        },
        {
            FileNameJoin@{$DocumentationBuildDirectory,
                "Internal", "web", "html", "images", "mathematicaImages"
            },
            FileNameJoin@{"images", "mathematicaImages"},
            "bullet.gif"
        }
    },
    "DeletableFileNames" -> {
        "*.nb",
        "_*.cfs", "segments_*", "segments.gen",
        "*.html", "*.txt", "*.js", "*.json", "*.css", "*.png", "*.gif", "*.jpg",
        "*.svg", "*.eot", "*.otf", "*.ttf", "*.woff"
    }
};
buildDocumentation[appDir_String?DirectoryQ, opts : OptionsPattern[]] :=
    With[
        {
            appName = Replace[nodefQuiet@OptionValue@"ApplicationName", {
                Automatic :> FileNameTake@appDir,
                nonStrOptRule[buildDocumentation::optAutStr,
                    "ApplicationName", Return[$Failed, With]
                ]
            }],
            srcDir = useProcessDirectory[
                Return[$Failed, With],
                appDir,
                nodefQuiet@OptionValue@"ProcessDirectory"
            ]@ExpandFileName@Replace[nodefQuiet@OptionValue@"SourceDirectory",
                nonStrOptRule[buildDocumentation::optStr,
                    "SourceDirectory", Return[$Failed, With]
                ]
            ]
        },
        (* Call buildDocumentation for all language specific
           documentation directories. *)
        Cases[FileNames[Keys@$languages, srcDir],
            inputDir_?DirectoryQ :> With[{lang = FileNameTake@inputDir},
                lang -> buildDocumentation[
                    appDir, inputDir, appName, lang, opts
                ]
            ]
        ]
    ]
buildDocumentation[
    appDir_String?DirectoryQ, inputDir_String?DirectoryQ,
    appName : Except["", _String], lang:(_String?validLangQ):"English",
    opts : OptionsPattern[]
] :=
    Module[
        {
            buildDir, nbOutputDir, htmlOutputDir, incremental, buildIndex,
            completeHTMLQ, fullOpts, nbBuilder, htmlExporter, results, tag
        },
        useProcessDirectory[
            Return[$Failed, Module], appDir,
            nodefQuiet@OptionValue@"ProcessDirectory"
        ]@docBuildEnv[
            appName, inputDir, lang, FilterRules[{opts}, Options@docBuildEnv]
        ]@Catch[
            buildDir = ExpandFileName@Replace[
                nodefQuiet@OptionValue@"BuildDirectory",
                nonStrOptRule[buildDocumentation::optStr,
                    "BuildDirectory", Throw[$Failed, tag]
                ]
            ];
            nbOutputDir = ExpandFileName@Replace[
                nodefQuiet@OptionValue@"NBOutputDirectory", {
                Automatic :> FileNameJoin@
                    {buildDir, appName, "Documentation", lang},
                nonStrOptRule[buildDocumentation::optAutStr,
                    "NBOutputDirectory", Throw[$Failed, tag]
                ]
            }];
            htmlOutputDir = ExpandFileName@Replace[
                nodefQuiet@OptionValue@"HTMLOutputDirectory", {
                Automatic :> FileNameJoin@{buildDir, appName <> "-HTML", lang},
                nonStrOptRule[buildDocumentation::optAutStr,
                    "HTMLOutputDirectory", Throw[$Failed, tag]
                ]
            }];
            {incremental, buildIndex, completeHTMLQ} = MapThread[
                If[BooleanQ@#2,
                    #2
                (* else *),
                    Message[buildDocumentation::opttf,
                        HoldForm@#1, HoldForm@#2
                    ];
                    Throw[$Failed, tag]
                ]&
                ,
                {#, nodefQuiet@OptionValue@#}&@
                    {"Incremental", "BuildIndex", "CompleteHTMLQ"}
            ];
            fullOpts = Flatten@
                {"Language" -> lang, opts, Options@buildDocumentation};
            nbBuilder = nodefQuiet[OptionValue@"NBBuilder"][
                nbOutputDir, fullOpts
            ];
            htmlExporter = nodefQuiet[OptionValue@"HTMLExporter"][
                appName, htmlOutputDir, fullOpts
            ];

            If[Not@incremental,
                (* Clean all output directories. *)
                Scan[
                    throwOnFailed[tag]@cleanDir[#,
                        nodefQuiet@OptionValue@"DeletableFileNames"
                    ]&,
                    Union@{buildDir, nbOutputDir, htmlOutputDir}
                ]
            ];

            results = Select[
                notebookFileNames[inputDir, Sequence @@ fullOpts],
                StringMatchQ["*.nb"]
            ] // Map[
                toFileObject[] /*
                getDocPath[inputDir] /*
                If[incremental,
                    ifNewerThan[builtNBPath@nbOutputDir,
                        nbBuilder
                    (* else *),
                        renameTo[builtNBPath@nbOutputDir]
                    ] /*
                    ifNewerThan[builtHTMLPath@htmlOutputDir,
                        functionalIf[Not @* KeyExistsQ["Notebook"],
                            getDocNotebook[False, fullOpts]
                        ] /*
                        htmlExporter
                    (* else *),
                        renameTo[builtHTMLPath@htmlOutputDir]
                    ]
                (* else *),
                    nbBuilder /*
                    htmlExporter
                ] /*
                (Flatten@{#History, #Path}&)
            ];

            If[buildIndex,
                DocumentationBuild`FunctionPaclets`Private`indexNotebooks[
                    Cases[results, {_, builtNB_String, ___} :> builtNB],
                    nbOutputDir,
                    lang
                ]
            ];

            If[completeHTMLQ,
                Cases[nodefQuiet@OptionValue@"WebAssets",
                    {fromDir_String, toDir_String, rest___} :>
                        copyFiles[
                            fromDir,
                            FileNameJoin@{htmlOutputDir, toDir},
                            rest
                        ],
                    {0, 1}
                ]
            ];

            results
            ,
            tag
        ]
    ]

Building documentation for MyApplication application, with standard directory structure, can be performed in following way:

buildDocumentation["path/to/MyApplication"]

It will build all documentation source notebooks from path/to/MyApplication/Documentation/<language> directories and save result together with search index in path/to/build/MyApplication/Documentation directory and HTML version together with web assets in path/to/build/MyApplication-HTML.

Your Answer

 
discard

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.