diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 13a46fa5b1f..d504437ae0f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -36,7 +36,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # For authentication with GitHub Actions token DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key DOCUMENTER_LATEX_DEBUG: ${{ github.workspace }}/latex-debug-logs - run: julia --color=yes --project=docs/ docs/make.jl + run: julia --color=yes --project=docs -p 2 docs/make.jl - uses: actions/upload-artifact@v6 if: ${{ always() }} with: diff --git a/docs/.gitignore b/docs/.gitignore index 6c50ace4b86..c338b3d3f72 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,5 +1,6 @@ build/ latex_build/ +latex_src/ src/tutorials/*/*.md !src/tutorials/*/introduction.md !src/tutorials/algorithms/parallelism.md diff --git a/docs/DocumenterReference.jl b/docs/DocumenterReference.jl index 3fadca62fcd..31456ff2ed0 100644 --- a/docs/DocumenterReference.jl +++ b/docs/DocumenterReference.jl @@ -21,7 +21,6 @@ import MarkdownAST struct _Config current_module::Module - root::String subdirectory::String modules::Dict{Module,<:Vector} sort_by::Function @@ -35,7 +34,6 @@ Documenter.Selectors.order(::Type{APIBuilder}) = 0.0 """ automatic_reference_documentation(; - root::String, subdirectory::String, modules::Dict{Module,Vector{Pair{String,DocType}}}, sort_by::Function = identity, @@ -60,7 +58,6 @@ Each time you call this function, a new object is added to the global variable `DocumenterReference.CONFIG`. """ function automatic_reference_documentation(; - root::String, subdirectory::String, modules::Vector, sort_by::Function = identity, @@ -73,7 +70,6 @@ function automatic_reference_documentation(; current_module = first(_to_extras(m)) pages = _automatic_reference_documentation( current_module; - root, subdirectory, modules = _modules, sort_by, @@ -85,12 +81,11 @@ end function _automatic_reference_documentation( current_module::Module; - root::String, subdirectory::String, modules::Dict{Module,<:Vector}, sort_by::Function, ) - push!(CONFIG, _Config(current_module, root, subdirectory, modules, sort_by)) + push!(CONFIG, _Config(current_module, subdirectory, modules, sort_by)) return "$subdirectory/$current_module.md" end diff --git a/docs/make.jl b/docs/make.jl index 1d22040d2cc..c213c1bb7c7 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -3,802 +3,29 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at https://mozilla.org/MPL/2.0/. -import Documenter -import DocumenterCitations -import Downloads -import Gurobi -import Literate -import MathOptInterface -import Pkg -import Test -import TOML -import tectonic_jll +import Distributed -using JuMP -using JuMP.Containers - -# Pass --fast as an argument to skip rebuilding the examples and running -# doctests. Only use this argument to rapidly test small changes to the -# Markdown. _Never_ set it in production. -const _FAST = findfirst(isequal("--fast"), ARGS) !== nothing - -# Pass --fix to run with `doctests=:fix`. -const _FIX = findfirst(isequal("--fix"), ARGS) !== nothing - -# A flag to check if we are running in a GitHub action. -const _IS_GITHUB_ACTIONS = get(ENV, "GITHUB_ACTIONS", "false") == "true" - -# Pass --pdf to build the PDF. On GitHub actions, we always build the PDF. -const _PDF = findfirst(isequal("--pdf"), ARGS) !== nothing || _IS_GITHUB_ACTIONS - -# Exclude these files because they require ${{ secrets.GUROBI_LICENSE }}, which -# is not available to forks. -const _HAS_GUROBI = try - Gurobi.Env() - true -catch - false -end -@show _HAS_GUROBI - -const _GUROBI_EXCLUDES = String[] -if !_HAS_GUROBI - push!(_GUROBI_EXCLUDES, "callbacks") -end - -# ============================================================================== -# Run literate.jl -# ============================================================================== - -function _link_example(content) - edit_url = match(r"EditURL = \"(.+?)\"", content)[1] - if !_IS_GITHUB_ACTIONS - # The link won't work locally. So hard-code in a URL. - edit_url = replace( - edit_url, - "" => "https://github.com/jump-dev/JuMP.jl/tree/master", - ) - end - title_line = findfirst(r"\n# .+?\n", content) - line = content[title_line] - new_title = string( - line, - "\n", - "_This tutorial was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl)._\n", - "[_Download the source as a `.jl` file_]($edit_url).\n", - ) - return replace(content, line => new_title) -end - -function _file_list(full_dir, relative_dir, extension) - function filter_fn(filename) - if !endswith(filename, extension) - return false - elseif _HAS_GUROBI - return true - end - return all(f -> !endswith(filename, f * extension), _GUROBI_EXCLUDES) - end - return map(filter!(filter_fn, sort!(readdir(full_dir)))) do file - return joinpath(relative_dir, file) - end -end - -""" - _include_sandbox(filename) - -Include the `filename` in a temporary module that acts as a sandbox. (Ensuring -no constants or functions leak into other files.) -""" -function _include_sandbox(filename) - mod = @eval module $(gensym()) end - return Base.include(mod, filename) -end - -function _literate_directory(dir) - for filename in _file_list(dir, dir, ".md") - if endswith(filename, "introduction.md") - continue - elseif endswith(filename, "parallelism.md") - continue - else - rm(filename) - end - end - for filename in _file_list(dir, dir, ".jl") - Literate.markdown( - filename, - dir; - documenter = true, - postprocess = _link_example, - # Turn off the footer. We manually add a modified one. - credit = false, - ) - end - return nothing -end - -if !_FAST - for (root, dir, files) in walkdir(joinpath(@__DIR__, "src", "tutorials")) - _literate_directory.(joinpath.(root, dir)) - end - # Convert `@example` blocks into `@repl` blocks in the following files: - for file in [ - joinpath("getting_started", "getting_started_with_julia.md"), - joinpath("getting_started", "getting_started_with_JuMP.md"), - joinpath("getting_started", "debugging.md"), - joinpath("getting_started", "performance_tips.md"), - joinpath("linear", "tips_and_tricks.md"), - joinpath("linear", "typed_indices.md"), - ] - filename = joinpath(@__DIR__, "src", "tutorials", file) - content = read(filename, String) - content = replace(content, "@example" => "@repl") - write(filename, content) - end -end - -function _add_edit_url(filename, url) - contents = read(filename, String) - open(filename, "w") do io - write(io, "```@meta\nEditURL = \"$url\"\n```\n\n") - write(io, contents) - return - end - return -end - -# ============================================================================== -# Add tutorial overview -# ============================================================================== - -package_to_headers = Dict{String,Vector{String}}() - -for (root, dirs, files) in walkdir(joinpath(@__DIR__, "src", "tutorials")) - for file in filter(f -> endswith(f, ".jl") | endswith(f, ".md"), files) - contents = read(joinpath(root, file), String) - if startswith(contents, "```@meta") - continue # Already literated - end - m = match(r"\n# # (.+)\n", contents) - if m === nothing - m = match(r"^# (.+)\n", contents) - end - title = m[1] - anchor = "[$title](@ref)" - if occursin("@id", title) - m = match(r"\[(.+)\]\(@id (.+)\)", title) - anchor = "[$(m[1])](@ref $(m[2]))" - end - import_regex = r"\nimport ([a-z][A-Z]+)( as [a-zA-Z]+)?\n"i - for m in eachmatch(import_regex, contents; overlap = true) - push!(get!(package_to_headers, m[1], String[]), anchor) - end - end -end - -open(joinpath(@__DIR__, "src", "tutorials", "overview.md"), "w") do io - println( - io, - """ - # Tutorials by package - - To make it easier to find relevant tutorials, this page lists all the tutorials - by the packages they import. - - For example, to find tutorials that solve nonlinear programs, look at the - [Ipopt.jl](@ref tutorial_Ipopt) section. - """, - ) - for (pkg, tutorials) in sort!(collect(package_to_headers)) - if pkg == "JuMP" - continue - end - println(io, "## [$(pkg).jl](@id tutorial_$pkg)") - println(io, join(unique(sort!(tutorials)), " • ")) - println(io) - end - return -end - -# ============================================================================== -# Add solver README -# ============================================================================== - -const _LIST_OF_SOLVERS = Pair{String,String}[] -const _LIST_OF_EXTENSIONS = Pair{String,String}[] -for (solver, data) in TOML.parsefile(joinpath(@__DIR__, "packages.toml")) - user = get(data, "user", "jump-dev") - tag = data["rev"] - filename = get(data, "filename", "README.md") - out_filename = joinpath(@__DIR__, "src", "packages", "$solver.md") - user_repo = string(user, "/", solver, get(data, "ext", ".jl")) - Downloads.download( - "https://raw.githubusercontent.com/$user_repo/$tag/$filename", - out_filename; - headers = Dict("Authorization" => "token " * ENV["GITHUB_TOKEN"]), - ) - _add_edit_url( - out_filename, - "https://github.com/$user_repo/blob/$tag/$filename", - ) - if get(data, "has_html", false) == true - # Very simple detector of HTML to wrap in ```@raw html - lines = readlines(out_filename) - open(out_filename, "w") do io - closing_tag = nothing - math_open = false - for line in lines - line = replace( - line, - "#gh-light-mode-only\"" => "\" class=\"display-light-only\"", - ) - line = replace( - line, - "#gh-dark-mode-only\"" => "\" class=\"display-dark-only\"", - ) - for ext in ("svg", "png", "gif") - line = replace(line, ".$ext\"" => ".$ext?raw=true\"") - end - if line == "\$\$" - line = math_open ? "```" : "```math" - math_open = !math_open - end - tag = if startswith(line, "" - else - m = match(r"\<([a-z0-9]+)", line) - m === nothing ? nothing : "" - end - if closing_tag === nothing && tag !== nothing - println(io, "```@raw html") - closing_tag = tag - end - println(io, line) - if closing_tag !== nothing && endswith(line, closing_tag) - println(io, "```") - closing_tag = nothing - end - end - end - end - if get(data, "extension", false) - push!(_LIST_OF_EXTENSIONS, user_repo => "packages/$solver.md") - else - push!(_LIST_OF_SOLVERS, user_repo => "packages/$solver.md") - end -end -push!( - _LIST_OF_EXTENSIONS, - "rafaqz/DimensionalData.jl" => "extensions/DimensionalData.md", -) - -# Sort, with jump-dev repos at the start. -sort!(_LIST_OF_SOLVERS; by = x -> (!startswith(x[1], "jump-dev/"), x[1])) -sort!(_LIST_OF_EXTENSIONS; by = x -> (!startswith(x[1], "jump-dev/"), x[1])) -pushfirst!(_LIST_OF_SOLVERS, "Introduction" => "packages/solvers.md") -pushfirst!(_LIST_OF_EXTENSIONS, "Introduction" => "extensions/introduction.md") - -# ============================================================================== -# JuMP API -# ============================================================================== - -include(joinpath(@__DIR__, "DocumenterReference.jl")) - -function sort_by_api_fn((key, type)) - deprecated_methods = [ - :add_nonlinear_constraint, - :add_nonlinear_expression, - :add_nonlinear_parameter, - :all_nonlinear_constraints, - :get_optimizer_attribute, - :nonlinear_constraint_string, - :nonlinear_dual_start_value, - :nonlinear_expr_string, - :nonlinear_model, - :num_nonlinear_constraints, - :register, - :set_nonlinear_dual_start_value, - :set_nonlinear_objective, - :set_normalized_coefficients, - :set_optimizer_attribute, - :set_optimizer_attributes, - :set_value, - :NonlinearConstraintIndex, - :NonlinearConstraintRef, - :NonlinearExpression, - :NonlinearParameter, - ] - return startswith("$key", "@NL") || key in deprecated_methods -end - -jump_api_reference = DocumenterReference.automatic_reference_documentation(; - root = joinpath(@__DIR__, "src"), - subdirectory = "api", - modules = [ - JuMP => [ - "Base.empty!(::GenericModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - "Base.isempty(::GenericModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - "Base.copy(::AbstractModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - "Base.write(::IO, ::GenericModel; ::MOI.FileFormats.FileFormat)" => - DocumenterReference.DOCTYPE_FUNCTION, - # Documenter tries to add `read(::IO, ::Type)` instead of this - # specific method - # "Base.read(::IO, ::Type{<:GenericModel}; ::MOI.FileFormats.FileFormat)" => - # DocumenterReference.DOCTYPE_FUNCTION, - "MOI.Utilities.reset_optimizer(::GenericModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - "MOI.Utilities.drop_optimizer(::GenericModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - "MOI.Utilities.attach_optimizer(::GenericModel)" => - DocumenterReference.DOCTYPE_FUNCTION, - ], - JuMP.Containers => [ - # TODO(odow): consider exporting these from JuMP.Containers. - "Containers.@container" => DocumenterReference.DOCTYPE_MACRO, - "Containers.container" => DocumenterReference.DOCTYPE_FUNCTION, - "Containers.rowtable" => DocumenterReference.DOCTYPE_FUNCTION, - "Containers.default_container" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.nested" => DocumenterReference.DOCTYPE_FUNCTION, - "Containers.vectorized_product" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.build_error_fn" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.parse_macro_arguments" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.parse_ref_sets" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.build_name_expr" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.add_additional_args" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.container_code" => - DocumenterReference.DOCTYPE_FUNCTION, - "Containers.AutoContainerType" => - DocumenterReference.DOCTYPE_STRUCT, - "Containers.NestedIterator" => - DocumenterReference.DOCTYPE_STRUCT, - "Containers.VectorizedProductIterator" => - DocumenterReference.DOCTYPE_STRUCT, - ], - ], - sort_by = sort_by_api_fn, -) - -# ============================================================================== -# JuMP documentation structure -# ============================================================================== - -# This constant dictates the layout of the documentation. It is manually -# constructed so that we can have control over the order in which pages are -# shown. If you add a new page to the documentation, make sure to add it here! -# -# !!! warning -# If you move any of the top-level chapters around, make sure to update the -# index of the "release_notes.md" in the section which builds the PDF. -const _PAGES = [ - "Introduction" => ["index.md", "should_i_use.md", "installation.md"], - "Tutorials" => [ - "tutorials/overview.md", - "Getting started" => [ - "tutorials/getting_started/introduction.md", - "tutorials/getting_started/getting_started_with_julia.md", - "tutorials/getting_started/getting_started_with_JuMP.md", - "tutorials/getting_started/getting_started_with_sets_and_indexing.md", - "tutorials/getting_started/getting_started_with_data_and_plotting.md", - "tutorials/getting_started/debugging.md", - "tutorials/getting_started/tolerances.md", - "tutorials/getting_started/design_patterns_for_larger_models.md", - "tutorials/getting_started/performance_tips.md", - "tutorials/getting_started/sum_if.md", - ], - "Transitioning" => - ["tutorials/transitioning/transitioning_from_matlab.md"], - "Linear programs" => [ - "tutorials/linear/introduction.md", - "tutorials/linear/knapsack.md", - "tutorials/linear/diet.md", - "tutorials/linear/cannery.md", - "tutorials/linear/typed_indices.md", - "tutorials/linear/factory_schedule.md", - "tutorials/linear/multi.md", - "tutorials/linear/multi_commodity_network.md", - "tutorials/linear/tips_and_tricks.md", - "tutorials/linear/piecewise_linear.md", - "tutorials/linear/facility_location.md", - "tutorials/linear/finance.md", - "tutorials/linear/geographic_clustering.md", - "tutorials/linear/network_flows.md", - "tutorials/linear/transp.md", - "tutorials/linear/multi_objective_examples.md", - "tutorials/linear/multi_objective_knapsack.md", - "tutorials/linear/multi_objective_project_planning.md", - "tutorials/linear/sudoku.md", - "tutorials/linear/n-queens.md", - "tutorials/linear/constraint_programming.md", - "tutorials/linear/callbacks.md", - "tutorials/linear/lp_sensitivity.md", - "tutorials/linear/basis.md", - "tutorials/linear/mip_duality.md", - "tutorials/linear/multiple_solutions.md", - ], - "Nonlinear programs" => [ - "tutorials/nonlinear/introduction.md", - "tutorials/nonlinear/simple_examples.md", - "tutorials/nonlinear/tips_and_tricks.md", - "tutorials/nonlinear/operator_ad.md", - "tutorials/nonlinear/user_defined_hessians.md", - "tutorials/nonlinear/nested_problems.md", - "tutorials/nonlinear/querying_hessians.md", - "tutorials/nonlinear/complementarity.md", - "tutorials/nonlinear/classifiers.md", - "tutorials/nonlinear/portfolio.md", - "tutorials/nonlinear/rocket_control.md", - "tutorials/nonlinear/space_shuttle_reentry_trajectory.md", - "tutorials/nonlinear/vector_nonlinear_oracle.md", - ], - "Conic programs" => [ - "tutorials/conic/introduction.md", - "tutorials/conic/tips_and_tricks.md", - "tutorials/conic/dualization.md", - "tutorials/conic/arbitrary_precision.md", - "tutorials/conic/start_values.md", - "tutorials/conic/simple_examples.md", - "tutorials/conic/chordal_decomposition.md", - "tutorials/conic/logistic_regression.md", - "tutorials/conic/experiment_design.md", - "tutorials/conic/min_ellipse.md", - "tutorials/conic/ellipse_approx.md", - "tutorials/conic/ellipse_fitting.md", - "tutorials/conic/quantum_discrimination.md", - ], - "Algorithms" => [ - "tutorials/algorithms/benders_decomposition.md", - "tutorials/algorithms/cutting_stock_column_generation.md", - "tutorials/algorithms/tsp_lazy_constraints.md", - "tutorials/algorithms/rolling_horizon.md", - "tutorials/algorithms/parallelism.md", - "tutorials/algorithms/pdhg.md", - "tutorials/algorithms/progressive_hedging.md", - ], - "Applications" => [ - "tutorials/applications/power_systems.md", - "tutorials/applications/optimal_power_flow.md", - "tutorials/applications/web_app.md", - "tutorials/applications/two_stage_stochastic.md", - ], - ], - "Manual" => [ - "manual/models.md", - "manual/variables.md", - "manual/constraints.md", - "manual/expressions.md", - "manual/objective.md", - "manual/containers.md", - "manual/solutions.md", - "manual/callbacks.md", - "manual/complex.md", - "manual/nonlinear.md", - "manual/nlp.md", - ], - jump_api_reference, - "Background Information" => [ - "background/algebraic_modeling_languages.md", - "background/bibliography.md", - ], - "Developer Docs" => [ - "Contributing" => "developers/contributing.md", - "Extensions" => "developers/extensions.md", - "Custom binaries" => "developers/custom_solver_binaries.md", - "Style Guide" => "developers/style.md", - "Conventions" => "developers/conventions.md", - "Roadmap" => "developers/roadmap.md", - "Checklists" => "developers/checklists.md", - ], - "Solvers" => _LIST_OF_SOLVERS, - "Extensions" => _LIST_OF_EXTENSIONS, - # "release_notes.md", # To be added later -] - -# ============================================================================== -# Modify the release notes -# ============================================================================== - -function fix_release_line( - line::String, - url::String = "https://github.com/jump-dev/JuMP.jl", -) - # (#XXXX) -> ([#XXXX](url/issue/XXXX)) - while (m = match(r"\(\#([0-9]+)\)", line)) !== nothing - id = m.captures[1] - line = replace(line, m.match => "([#$id]($url/issues/$id))") - end - # ## Version X.Y.Z -> [Version X.Y.Z](url/releases/tag/vX.Y.Z) - while (m = match(r"\#\# Version ([0-9]+.[0-9]+.[0-9]+)", line)) !== nothing - tag = m.captures[1] - line = replace( - line, - m.match => "## [Version $tag]($url/releases/tag/v$tag)", - ) - end - # ## vX.Y.Z -> [vX.Y.Z](url/releases/tag/vX.Y.Z) - while (m = match(r"\#\# (v[0-9]+.[0-9]+.[0-9]+)", line)) !== nothing - tag = m.captures[1] - line = replace(line, m.match => "## [$tag]($url/releases/tag/$tag)") - end - return line -end - -function _fix_release_lines(changelog, release_notes, args...) - open(release_notes, "w") do io - for line in readlines(changelog; keep = true) - write(io, fix_release_line(line, args...)) - end - end - return -end - -_fix_release_lines( - joinpath(@__DIR__, "src", "changelog.md"), - joinpath(@__DIR__, "src", "release_notes.md"), -) - -_add_edit_url(joinpath(@__DIR__, "src", "release_notes.md"), "changelog.md") - -# ============================================================================== -# Embed MathOptInterface.jl documentation -# ============================================================================== - -function _add_moi_pages() - moi_dir = joinpath(@__DIR__, "src", "moi") - try - rm(moi_dir; recursive = true) - catch - end - moi_docs = joinpath(dirname(dirname(pathof(MOI))), "docs") - cp(joinpath(moi_docs, "src"), moi_dir; force = true) - # Files in `moi_docs` are probably in read-only mode (`0o444`). Let's give - # ourselves write permission. - chmod(moi_dir, 0o777; recursive = true) - make = read(joinpath(moi_docs, "make.jl"), String) - # Match from `_PAGES = [` until the start of in `# =====` - s = strip(match(r"_PAGES = (\[.+?)\#"s, make)[1]) - # Rename every file to the `moi/` directory. - for m in eachmatch(r"\"([a-zA-Z\_\/]+?\.md)\"", s) - s = replace(s, m[1] => "moi/" * m[1]) - end - push!(_PAGES, "MathOptInterface" => eval(Meta.parse(s))) - # Update the intro of the MOI docs. - src = """# Introduction - - Welcome to the documentation for MathOptInterface. - - !!! note - This documentation is also available in PDF format: - [MathOptInterface.pdf](MathOptInterface.pdf).""" - status = sprint(io -> Pkg.status("MathOptInterface"; io = io)) - version = match(r"(v[0-9].[0-9]+.[0-9]+)", status)[1] - dest = """# [Introduction](@id moi_documentation) - - !!! warning - This documentation in this section is a copy of the official - MathOptInterface documentation available at - [https://jump.dev/MathOptInterface.jl/$version](https://jump.dev/MathOptInterface.jl/$version). - It is included here to make it easier to link concepts between JuMP and - MathOptInterface. - """ - index_filename = joinpath(moi_dir, "index.md") - content = replace(read(index_filename, String), src => dest) - write(index_filename, content) - _fix_release_lines( - joinpath(moi_dir, "changelog.md"), - joinpath(moi_dir, "release_notes.md"), - "https://github.com/jump-dev/MathOptInterface.jl", - ) - for (root, dirs, files) in walkdir(moi_dir) - for file in files - if endswith(file, ".md") - filename = joinpath(root, file) - moi_filename = replace(filename, moi_dir => "") - _add_edit_url( - filename, - "https://github.com/jump-dev/MathOptInterface.jl/blob/$version/docs/src$moi_filename", - ) - end - end - end - for (filename, replacements) in [ - "background/infeasibility_certificates.md" => [ - r"^# Infeasibility certificates$"m => "# [Infeasibility certificates](@id moi_infeasibility_certificates)", - ], - "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], - # This can be removed once we support JSON@1 in the documentation - "submodules/FileFormats/overview.md" => ["JSON.Object" => "Dict"], - # These can be removed once we support MOI@1.48.1 or later - "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], - "submodules/Nonlinear/SymbolicAD.md" => [ - "# `simplify`" => "# [`simplify`](@id symbolic_ad_manual_simplify)", - "# `variables`" => "# [`variables`](@id symbolic_ad_manual_variables)", - "# `derivative`" => "# [`derivative`](@id symbolic_ad_manual_derivative)", - ], - ] - filename = joinpath(moi_dir, filename) - write(filename, replace(read(filename, String), replacements...)) - end - return -end - -_add_moi_pages() - -# ============================================================================== -# Check that we have included all the markdown files in _PAGES! -# ============================================================================== - -# For Documenter.hide arguments -function _add_to_set( - set::Set{String}, - arg::Tuple{Bool,String,String,Vector{Any}}, -) - _add_to_set(set, arg[3]) - return -end -_add_to_set(set, filename::String) = push!(set, filename) -_add_to_set(set, filename::Pair) = _add_to_set(set, filename[2]) -_add_to_set(set, filename::Vector) = _add_to_set.(Ref(set), filename) - -function _validate_pages() - set = Set{String}() - for page in _PAGES - _add_to_set(set, page) - end - missing_files = String[] - doc_src = joinpath(@__DIR__, "src", "") - for (root, dir, files) in walkdir(doc_src) - for file in files - if file == "changelog.md" || file == "release_notes.md" - continue - end - filename = replace(joinpath(root, file), doc_src => "") - if endswith(filename, ".md") && !(filename in set) - push!(missing_files, filename) - end - end - end - if !isempty(missing_files) - error("Some files missing from documentation: $(missing_files)") - end - return -end - -_validate_pages() - -# ============================================================================== -# Build the HTML docs -# ============================================================================== - -Documenter.DocMeta.setdocmeta!( - JuMP, - :DocTestSetup, - :(using JuMP, JuMP.Containers); - recursive = true, -) - -Documenter.DocMeta.setdocmeta!( - MathOptInterface, - :DocTestSetup, - :(import MathOptInterface as MOI); - recursive = true, -) +Distributed.@everywhere include(joinpath(@__DIR__, "make_utilities.jl")) # Needed to make Documenter think that there is a PDF in the right place when -# link checking. Inn production we replace this by running the LaTeX build. +# link checking. In production we replace this by running the LaTeX build. write(joinpath(@__DIR__, "src", "JuMP.pdf"), "") -# Create remotes for Documenter -status = sprint(io -> Pkg.status("MathOptInterface"; io = io)) -version = match(r"(v[0-9].[0-9]+.[0-9]+)", status)[1] -gh_moi = Documenter.Remotes.GitHub("jump-dev", "MathOptInterface.jl") -remotes = Dict(pkgdir(MOI) => (gh_moi, version)) - -@time Documenter.makedocs( - sitename = "JuMP", - authors = "The JuMP core developers and contributors", - format = Documenter.HTML(; - # See https://github.com/JuliaDocs/Documenter.jl/issues/868 - prettyurls = get(ENV, "CI", nothing) == "true", - analytics = "G-0RZ8X3D3D0", - mathengine = Documenter.MathJax2(), - collapselevel = 1, - assets = ["assets/extra_styles.css", "assets/citations.css"], - sidebar_sitename = false, - # Do no check for large pages. - size_threshold_ignore = [ - "changelog.md", - "release_notes.md", - "api/JuMP.md", - "tutorials/getting_started/getting_started_with_data_and_plotting.md", - "moi/changelog.md", - "moi/release_notes.md", - "moi/reference/models.md", - "moi/reference/standard_form.md", - "moi/submodules/Bridges/list_of_bridges.md", - "moi/submodules/Bridges/reference.md", - "moi/submodules/Utilities/reference.md", - ], - ), - modules = [JuMP, MOI], - checkdocs = :none, - # Skip doctests if --fast provided. - doctest = _FIX ? :fix : !_FAST, - pages = vcat(_PAGES, "release_notes.md"), - remotes = remotes, - plugins = [ - DocumenterCitations.CitationBibliography( - joinpath(@__DIR__, "src", "references.bib"); - style = :authoryear, - ), - ], -) - -# ============================================================================== -# Build the LaTeX docs (if needed) -# ============================================================================== - -function _remove_literate_footer(dir) - for filename in _file_list(dir, dir, ".md") - file = read(filename, String) - index = findfirst( - "---\n\n!!! tip\n This tutorial was generated using [Literate", - file, - ) - if index !== nothing - write(filename, file[1:(first(index)-1)]) - end - end - return -end - -if _PDF - for (root, dir, files) in walkdir(joinpath(@__DIR__, "src", "tutorials")) - _remove_literate_footer.(joinpath.(root, dir)) - end - moi = pop!(_PAGES) # remove /MathOptInterface - pop!(moi[2]) # remove /MathOptInterface/release_notes.md - pop!(_PAGES) # remove /Extensions - pop!(_PAGES) # remove /Solvers - push!(_PAGES, moi) # Re-add /MathOptInterface - section_title, contents = _PAGES[4] - @assert section_title == "API Reference" - # `contents` is a big list of docstrings. By default, they'll - # show up at the `\chapter` level. That's too high. - _PAGES[4] = section_title => ["Docstrings" => contents] - latex_platform = if _IS_GITHUB_ACTIONS - Documenter.LaTeX(; - platform = "tectonic", - tectonic = tectonic_jll.tectonic(), - ) - else - Documenter.LaTeX(; platform = "native") - end - - @time Documenter.makedocs( - sitename = "JuMP", - authors = "The JuMP core developers and contributors", - format = latex_platform, - build = "latex_build", - pages = _PAGES, - debug = true, - plugins = [ - DocumenterCitations.CitationBibliography( - joinpath(@__DIR__, "src", "references.bib"); - style = :authoryear, - ), - ], - ) +@time begin + literate_tutorials() + add_tutorial_overview() + add_solver_readmes() + add_changelog() + add_moi_pages() +end + +@time if _PDF || _IS_GITHUB_ACTIONS + # Copy the src into a new directory for the latex build + cp(joinpath(@__DIR__, "src"), joinpath(@__DIR__, "latex_src"); force = true) + f_latex = Distributed.@spawnat :any make_latex() + f_html = Distributed.@spawnat :any make_html() + fetch(f_latex) + fetch(f_html) # Hack for deploying: copy the pdf (and only the PDF) into the HTML build # directory! We don't want to copy everything in `latex_build` because it # includes lots of extraneous LaTeX files. @@ -807,13 +34,14 @@ if _PDF joinpath(@__DIR__, "build", "JuMP.pdf"); force = true, ) + rm(joinpath(@__DIR__, "latex_src"); force = true, recursive = true) +else + @time make_html() end -# ============================================================================== -# Deploy everything in `build` -# ============================================================================== - -Documenter.deploydocs(; - repo = "github.com/jump-dev/JuMP.jl.git", - push_preview = true, -) +if _IS_GITHUB_ACTIONS + Documenter.deploydocs(; + repo = "github.com/jump-dev/JuMP.jl.git", + push_preview = true, + ) +end diff --git a/docs/make_utilities.jl b/docs/make_utilities.jl new file mode 100644 index 00000000000..c83f524db73 --- /dev/null +++ b/docs/make_utilities.jl @@ -0,0 +1,780 @@ +# Copyright 2017, Iain Dunning, Joey Huchette, Miles Lubin, and contributors +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +import Documenter +import DocumenterCitations +import Downloads +import Gurobi +import Literate +import MathOptInterface +import Pkg +import TOML +import tectonic_jll +import Test + +using JuMP +using JuMP.Containers + +Documenter.DocMeta.setdocmeta!( + JuMP, + :DocTestSetup, + :(using JuMP, JuMP.Containers); + recursive = true, +) + +Documenter.DocMeta.setdocmeta!( + MathOptInterface, + :DocTestSetup, + :(import MathOptInterface as MOI); + recursive = true, +) + +# A flag to check if we are running in a GitHub action. +const _IS_GITHUB_ACTIONS = get(ENV, "GITHUB_ACTIONS", "false") == "true" + +# Pass --fix to run with `doctests=:fix`. +const _FIX = findfirst(isequal("--fix"), ARGS) !== nothing + +# Pass --pdf to build the PDF. +const _PDF = findfirst(isequal("--pdf"), ARGS) !== nothing + +# Exclude these files because they require ${{ secrets.GUROBI_LICENSE }}, which +# is not available to forks. +const _HAS_GUROBI = try + Gurobi.Env() + true +catch + false +end +@show _HAS_GUROBI + +include(joinpath(@__DIR__, "DocumenterReference.jl")) +empty!(DocumenterReference.CONFIG) + +# ============================================================================== +# Run literate.jl +# ============================================================================== + +function _link_example(content) + edit_url = match(r"EditURL = \"(.+?)\"", content)[1] + if !_IS_GITHUB_ACTIONS + # The link won't work locally. So hard-code in a URL. + edit_url = replace( + edit_url, + "" => "https://github.com/jump-dev/JuMP.jl/tree/master", + ) + end + line = content[findfirst(r"\n# .+?\n", content)] + new_title = string( + line, + "\n", + "_This tutorial was generated using [Literate.jl](https://github.com/fredrikekre/Literate.jl)._\n", + "[_Download the source as a `.jl` file_]($edit_url).\n", + ) + return replace(content, line => new_title) +end + +function _add_edit_url(filename, url) + contents = read(filename, String) + open(filename, "w") do io + write(io, "```@meta\nEditURL = \"$url\"\n```\n\n") + write(io, contents) + return + end + return +end + +function literate_tutorials() + tutorial_dir = joinpath(@__DIR__, "src", "tutorials") + for (root, dir, files) in walkdir(tutorial_dir) + for file in filter!(endswith(".jl"), files) + if !_HAS_GUROBI && file == "callbacks.jl" + continue # Don't run if Gurobi not present + end + Literate.markdown( + joinpath(root, file), + root; + documenter = true, + postprocess = _link_example, + # Turn off the footer. We manually add a modified one. + credit = false, + ) + end + end + # Convert `@example` blocks into `@repl` blocks in the following files: + for file in [ + joinpath("getting_started", "getting_started_with_julia.md"), + joinpath("getting_started", "getting_started_with_JuMP.md"), + joinpath("getting_started", "debugging.md"), + joinpath("getting_started", "performance_tips.md"), + joinpath("linear", "tips_and_tricks.md"), + joinpath("linear", "typed_indices.md"), + ] + filename = joinpath(@__DIR__, "src", "tutorials", file) + content = read(filename, String) + content = replace(content, "@example" => "@repl") + write(filename, content) + end + return +end + +# ============================================================================== +# Add tutorial overview +# ============================================================================== + +function add_tutorial_overview() + package_to_headers = Dict{String,Vector{String}}() + for (root, dirs, files) in walkdir(joinpath(@__DIR__, "src", "tutorials")) + for file in filter(f -> endswith(f, ".jl") | endswith(f, ".md"), files) + contents = read(joinpath(root, file), String) + if startswith(contents, "```@meta") + continue # Already literated + end + m = match(r"\n# # (.+)\n", contents) + if m === nothing + m = match(r"^# (.+)\n", contents) + end + title = m[1] + anchor = "[$title](@ref)" + if occursin("@id", title) + m = match(r"\[(.+)\]\(@id (.+)\)", title) + anchor = "[$(m[1])](@ref $(m[2]))" + end + import_regex = r"\nimport ([a-z][A-Z]+)( as [a-zA-Z]+)?\n"i + for m in eachmatch(import_regex, contents; overlap = true) + push!(get!(package_to_headers, m[1], String[]), anchor) + end + end + end + open(joinpath(@__DIR__, "src", "tutorials", "overview.md"), "w") do io + println( + io, + """ + # Tutorials by package + + To make it easier to find relevant tutorials, this page lists all the tutorials + by the packages they import. + + For example, to find tutorials that solve nonlinear programs, look at the + [Ipopt.jl](@ref tutorial_Ipopt) section. + """, + ) + for (pkg, tutorials) in sort!(collect(package_to_headers)) + if pkg == "JuMP" + continue + end + println(io, "## [$(pkg).jl](@id tutorial_$pkg)") + println(io, join(unique(sort!(tutorials)), " • ")) + println(io) + end + return + end + return +end + +# ============================================================================== +# Add solver README +# ============================================================================== + +function add_solver_readmes() + for (solver, data) in TOML.parsefile(joinpath(@__DIR__, "packages.toml")) + user = get(data, "user", "jump-dev") + tag = data["rev"] + filename = get(data, "filename", "README.md") + out_filename = joinpath(@__DIR__, "src", "packages", "$solver.md") + user_repo = string(user, "/", solver, get(data, "ext", ".jl")) + Downloads.download( + "https://raw.githubusercontent.com/$user_repo/$tag/$filename", + out_filename; + headers = Dict("Authorization" => "token " * ENV["GITHUB_TOKEN"]), + ) + _add_edit_url( + out_filename, + "https://github.com/$user_repo/blob/$tag/$filename", + ) + if get(data, "has_html", false) == true + # Very simple detector of HTML to wrap in ```@raw html + lines = readlines(out_filename) + open(out_filename, "w") do io + closing_tag = nothing + math_open = false + for line in lines + line = replace( + line, + "#gh-light-mode-only\"" => "\" class=\"display-light-only\"", + ) + line = replace( + line, + "#gh-dark-mode-only\"" => "\" class=\"display-dark-only\"", + ) + for ext in ("svg", "png", "gif") + line = replace(line, ".$ext\"" => ".$ext?raw=true\"") + end + if line == "\$\$" + line = math_open ? "```" : "```math" + math_open = !math_open + end + tag = if startswith(line, "" + else + m = match(r"\<([a-z0-9]+)", line) + m === nothing ? nothing : "" + end + if closing_tag === nothing && tag !== nothing + println(io, "```@raw html") + closing_tag = tag + end + println(io, line) + if closing_tag !== nothing && endswith(line, closing_tag) + println(io, "```") + closing_tag = nothing + end + end + end + end + end + return +end + +# ============================================================================== +# Modify the release notes +# ============================================================================== + +function fix_release_line( + line::String, + url::String = "https://github.com/jump-dev/JuMP.jl", +) + # (#XXXX) -> ([#XXXX](url/issue/XXXX)) + while (m = match(r"\(\#([0-9]+)\)", line)) !== nothing + id = m.captures[1] + line = replace(line, m.match => "([#$id]($url/issues/$id))") + end + # ## Version X.Y.Z -> [Version X.Y.Z](url/releases/tag/vX.Y.Z) + while (m = match(r"\#\# Version ([0-9]+.[0-9]+.[0-9]+)", line)) !== nothing + tag = m.captures[1] + line = replace( + line, + m.match => "## [Version $tag]($url/releases/tag/v$tag)", + ) + end + # ## vX.Y.Z -> [vX.Y.Z](url/releases/tag/vX.Y.Z) + while (m = match(r"\#\# (v[0-9]+.[0-9]+.[0-9]+)", line)) !== nothing + tag = m.captures[1] + line = replace(line, m.match => "## [$tag]($url/releases/tag/$tag)") + end + return line +end + +function _fix_release_lines(changelog, release_notes, args...) + open(release_notes, "w") do io + for line in readlines(changelog; keep = true) + write(io, fix_release_line(line, args...)) + end + end + return +end + +function add_changelog() + _fix_release_lines( + joinpath(@__DIR__, "src", "changelog.md"), + joinpath(@__DIR__, "src", "release_notes.md"), + ) + _add_edit_url(joinpath(@__DIR__, "src", "release_notes.md"), "changelog.md") + return +end + +# ============================================================================== +# Embed MathOptInterface.jl documentation +# ============================================================================== + +function add_moi_pages() + moi_dir = joinpath(@__DIR__, "src", "moi") + try + rm(moi_dir; recursive = true) + catch + end + moi_docs = joinpath(dirname(dirname(pathof(MOI))), "docs") + cp(joinpath(moi_docs, "src"), moi_dir; force = true) + # Files in `moi_docs` are probably in read-only mode (`0o444`). Let's give + # ourselves write permission. + chmod(moi_dir, 0o777; recursive = true) + # Update the intro of the MOI docs. + src = """# Introduction + + Welcome to the documentation for MathOptInterface. + + !!! note + This documentation is also available in PDF format: + [MathOptInterface.pdf](MathOptInterface.pdf).""" + status = sprint(io -> Pkg.status("MathOptInterface"; io = io)) + version = match(r"(v[0-9].[0-9]+.[0-9]+)", status)[1] + dest = """# [Introduction](@id moi_documentation) + + !!! warning + This documentation in this section is a copy of the official + MathOptInterface documentation available at + [https://jump.dev/MathOptInterface.jl/$version](https://jump.dev/MathOptInterface.jl/$version). + It is included here to make it easier to link concepts between JuMP and + MathOptInterface. + """ + index_filename = joinpath(moi_dir, "index.md") + content = replace(read(index_filename, String), src => dest) + write(index_filename, content) + _fix_release_lines( + joinpath(moi_dir, "changelog.md"), + joinpath(moi_dir, "release_notes.md"), + "https://github.com/jump-dev/MathOptInterface.jl", + ) + for (root, dirs, files) in walkdir(moi_dir) + for file in files + if endswith(file, ".md") + filename = joinpath(root, file) + moi_filename = replace(filename, moi_dir => "") + _add_edit_url( + filename, + "https://github.com/jump-dev/MathOptInterface.jl/blob/$version/docs/src$moi_filename", + ) + end + end + end + for (filename, replacements) in [ + "background/infeasibility_certificates.md" => [ + r"^# Infeasibility certificates$"m => "# [Infeasibility certificates](@id moi_infeasibility_certificates)", + ], + "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], + # This can be removed once we support JSON@1 in the documentation + "submodules/FileFormats/overview.md" => ["JSON.Object" => "Dict"], + # These can be removed once we support MOI@1.48.1 or later + "reference/models.md" => ["# ResultStatusCode" => "# Result Status"], + "submodules/Nonlinear/SymbolicAD.md" => [ + "# `simplify`" => "# [`simplify`](@id symbolic_ad_manual_simplify)", + "# `variables`" => "# [`variables`](@id symbolic_ad_manual_variables)", + "# `derivative`" => "# [`derivative`](@id symbolic_ad_manual_derivative)", + ], + ] + filename = joinpath(moi_dir, filename) + write(filename, replace(read(filename, String), replacements...)) + end + return +end + +# ============================================================================== +# JuMP API +# ============================================================================== + +function sort_by_api_fn((key, type)) + deprecated_methods = [ + :add_nonlinear_constraint, + :add_nonlinear_expression, + :add_nonlinear_parameter, + :all_nonlinear_constraints, + :get_optimizer_attribute, + :nonlinear_constraint_string, + :nonlinear_dual_start_value, + :nonlinear_expr_string, + :nonlinear_model, + :num_nonlinear_constraints, + :register, + :set_nonlinear_dual_start_value, + :set_nonlinear_objective, + :set_normalized_coefficients, + :set_optimizer_attribute, + :set_optimizer_attributes, + :set_value, + :NonlinearConstraintIndex, + :NonlinearConstraintRef, + :NonlinearExpression, + :NonlinearParameter, + ] + return startswith("$key", "@NL") || key in deprecated_methods +end + +# ============================================================================== +# Check that we have included all the markdown files in _PAGES! +# ============================================================================== + +# For Documenter.hide arguments +function _add_to_set( + set::Set{String}, + arg::Tuple{Bool,String,String,Vector{Any}}, +) + _add_to_set(set, arg[3]) + return +end +_add_to_set(set, filename::String) = push!(set, filename) +_add_to_set(set, filename::Pair) = _add_to_set(set, filename[2]) +_add_to_set(set, filename::Vector) = _add_to_set.(Ref(set), filename) + +function validate_pages(pages, source) + set = Set{String}() + for page in pages + _add_to_set(set, page) + end + missing_files = String[] + doc_src = joinpath(source, "") + for (root, dir, files) in walkdir(doc_src) + for file in files + if file == "changelog.md" || file == "release_notes.md" + continue + end + filename = replace(joinpath(root, file), doc_src => "") + if endswith(filename, ".md") && !(filename in set) + push!(missing_files, filename) + end + end + end + if !isempty(missing_files) + error("Some files missing from documentation: $(missing_files)") + end + return +end + +# ============================================================================== +# JuMP documentation structure +# ============================================================================== + +function list_of_solvers_and_extensions() + _LIST_OF_SOLVERS = Pair{String,String}[] + _LIST_OF_EXTENSIONS = + Pair{String,String}["rafaqz/DimensionalData.jl"=>"extensions/DimensionalData.md",] + for (solver, data) in TOML.parsefile(joinpath(@__DIR__, "packages.toml")) + user = get(data, "user", "jump-dev") + user_repo = string(user, "/", solver, get(data, "ext", ".jl")) + if get(data, "extension", false) + push!(_LIST_OF_EXTENSIONS, user_repo => "packages/$solver.md") + else + push!(_LIST_OF_SOLVERS, user_repo => "packages/$solver.md") + end + end + # Sort, with jump-dev repos at the start. + sort!(_LIST_OF_SOLVERS; by = x -> (!startswith(x[1], "jump-dev/"), x[1])) + sort!(_LIST_OF_EXTENSIONS; by = x -> (!startswith(x[1], "jump-dev/"), x[1])) + pushfirst!(_LIST_OF_SOLVERS, "Introduction" => "packages/solvers.md") + pushfirst!( + _LIST_OF_EXTENSIONS, + "Introduction" => "extensions/introduction.md", + ) + return _LIST_OF_SOLVERS, _LIST_OF_EXTENSIONS +end + +function get_moi_pages() + moi_docs = joinpath(pkgdir(MOI), "docs") + make = read(joinpath(moi_docs, "make.jl"), String) + # Match from `_PAGES = [` until the start of in `# =====` + s = strip(match(r"_PAGES = (\[.+?)\#"s, make)[1]) + # Rename every file to the `moi/` directory. + for m in eachmatch(r"\"([a-zA-Z\_\/]+?\.md)\"", s) + s = replace(s, m[1] => "moi/" * m[1]) + end + return "MathOptInterface" => eval(Meta.parse(s)) +end + +# This constant dictates the layout of the documentation. It is manually +# constructed so that we can have control over the order in which pages are +# shown. If you add a new page to the documentation, make sure to add it here! +# +# !!! warning +# If you move any of the top-level chapters around, make sure to update the +# index of the "release_notes.md" in the section which builds the PDF. +function documentation_structure() + _LIST_OF_SOLVERS, _LIST_OF_EXTENSIONS = list_of_solvers_and_extensions() + jump_api_reference = DocumenterReference.automatic_reference_documentation(; + subdirectory = "api", + modules = [ + JuMP => [ + "Base.empty!(::GenericModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + "Base.isempty(::GenericModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + "Base.copy(::AbstractModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + "Base.write(::IO, ::GenericModel; ::MOI.FileFormats.FileFormat)" => + DocumenterReference.DOCTYPE_FUNCTION, + # Documenter tries to add `read(::IO, ::Type)` instead of this + # specific method + # "Base.read(::IO, ::Type{<:GenericModel}; ::MOI.FileFormats.FileFormat)" => + # DocumenterReference.DOCTYPE_FUNCTION, + "MOI.Utilities.reset_optimizer(::GenericModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + "MOI.Utilities.drop_optimizer(::GenericModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + "MOI.Utilities.attach_optimizer(::GenericModel)" => + DocumenterReference.DOCTYPE_FUNCTION, + ], + JuMP.Containers => [ + # TODO(odow): consider exporting these from JuMP.Containers. + "Containers.@container" => + DocumenterReference.DOCTYPE_MACRO, + "Containers.container" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.rowtable" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.default_container" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.nested" => DocumenterReference.DOCTYPE_FUNCTION, + "Containers.vectorized_product" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.build_error_fn" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.parse_macro_arguments" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.parse_ref_sets" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.build_name_expr" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.add_additional_args" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.container_code" => + DocumenterReference.DOCTYPE_FUNCTION, + "Containers.AutoContainerType" => + DocumenterReference.DOCTYPE_STRUCT, + "Containers.NestedIterator" => + DocumenterReference.DOCTYPE_STRUCT, + "Containers.VectorizedProductIterator" => + DocumenterReference.DOCTYPE_STRUCT, + ], + ], + sort_by = sort_by_api_fn, + ) + pages = [ + "Introduction" => + ["index.md", "should_i_use.md", "installation.md"], + "Tutorials" => [ + "tutorials/overview.md", + "Getting started" => [ + "tutorials/getting_started/introduction.md", + "tutorials/getting_started/getting_started_with_julia.md", + "tutorials/getting_started/getting_started_with_JuMP.md", + "tutorials/getting_started/getting_started_with_sets_and_indexing.md", + "tutorials/getting_started/getting_started_with_data_and_plotting.md", + "tutorials/getting_started/debugging.md", + "tutorials/getting_started/tolerances.md", + "tutorials/getting_started/design_patterns_for_larger_models.md", + "tutorials/getting_started/performance_tips.md", + "tutorials/getting_started/sum_if.md", + ], + "Transitioning" => [ + "tutorials/transitioning/transitioning_from_matlab.md", + ], + "Linear programs" => [ + "tutorials/linear/introduction.md", + "tutorials/linear/knapsack.md", + "tutorials/linear/diet.md", + "tutorials/linear/cannery.md", + "tutorials/linear/typed_indices.md", + "tutorials/linear/factory_schedule.md", + "tutorials/linear/multi.md", + "tutorials/linear/multi_commodity_network.md", + "tutorials/linear/tips_and_tricks.md", + "tutorials/linear/piecewise_linear.md", + "tutorials/linear/facility_location.md", + "tutorials/linear/finance.md", + "tutorials/linear/geographic_clustering.md", + "tutorials/linear/network_flows.md", + "tutorials/linear/transp.md", + "tutorials/linear/multi_objective_examples.md", + "tutorials/linear/multi_objective_knapsack.md", + "tutorials/linear/multi_objective_project_planning.md", + "tutorials/linear/sudoku.md", + "tutorials/linear/n-queens.md", + "tutorials/linear/constraint_programming.md", + "tutorials/linear/callbacks.md", + "tutorials/linear/lp_sensitivity.md", + "tutorials/linear/basis.md", + "tutorials/linear/mip_duality.md", + "tutorials/linear/multiple_solutions.md", + ], + "Nonlinear programs" => [ + "tutorials/nonlinear/introduction.md", + "tutorials/nonlinear/simple_examples.md", + "tutorials/nonlinear/tips_and_tricks.md", + "tutorials/nonlinear/operator_ad.md", + "tutorials/nonlinear/user_defined_hessians.md", + "tutorials/nonlinear/nested_problems.md", + "tutorials/nonlinear/querying_hessians.md", + "tutorials/nonlinear/complementarity.md", + "tutorials/nonlinear/classifiers.md", + "tutorials/nonlinear/portfolio.md", + "tutorials/nonlinear/rocket_control.md", + "tutorials/nonlinear/space_shuttle_reentry_trajectory.md", + "tutorials/nonlinear/vector_nonlinear_oracle.md", + ], + "Conic programs" => [ + "tutorials/conic/introduction.md", + "tutorials/conic/tips_and_tricks.md", + "tutorials/conic/dualization.md", + "tutorials/conic/arbitrary_precision.md", + "tutorials/conic/start_values.md", + "tutorials/conic/simple_examples.md", + "tutorials/conic/chordal_decomposition.md", + "tutorials/conic/logistic_regression.md", + "tutorials/conic/experiment_design.md", + "tutorials/conic/min_ellipse.md", + "tutorials/conic/ellipse_approx.md", + "tutorials/conic/ellipse_fitting.md", + "tutorials/conic/quantum_discrimination.md", + ], + "Algorithms" => [ + "tutorials/algorithms/benders_decomposition.md", + "tutorials/algorithms/cutting_stock_column_generation.md", + "tutorials/algorithms/tsp_lazy_constraints.md", + "tutorials/algorithms/rolling_horizon.md", + "tutorials/algorithms/parallelism.md", + "tutorials/algorithms/pdhg.md", + "tutorials/algorithms/progressive_hedging.md", + ], + "Applications" => [ + "tutorials/applications/power_systems.md", + "tutorials/applications/optimal_power_flow.md", + "tutorials/applications/web_app.md", + "tutorials/applications/two_stage_stochastic.md", + ], + ], + "Manual" => [ + "manual/models.md", + "manual/variables.md", + "manual/constraints.md", + "manual/expressions.md", + "manual/objective.md", + "manual/containers.md", + "manual/solutions.md", + "manual/callbacks.md", + "manual/complex.md", + "manual/nonlinear.md", + "manual/nlp.md", + ], + jump_api_reference, + "Background Information" => [ + "background/algebraic_modeling_languages.md", + "background/bibliography.md", + ], + "Developer Docs" => [ + "Contributing" => "developers/contributing.md", + "Extensions" => "developers/extensions.md", + "Custom binaries" => "developers/custom_solver_binaries.md", + "Style Guide" => "developers/style.md", + "Conventions" => "developers/conventions.md", + "Roadmap" => "developers/roadmap.md", + "Checklists" => "developers/checklists.md", + ], + "Solvers" => _LIST_OF_SOLVERS, + "Extensions" => _LIST_OF_EXTENSIONS, + "release_notes.md", + ] + return pages +end + +# ============================================================================== +# Build the HTML docs +# ============================================================================== + +function make_html() + pages = documentation_structure() + p = pop!(pages) + push!(pages, get_moi_pages()) + push!(pages, p) + validate_pages(pages, joinpath(@__DIR__, "src")) + # Create remotes for Documenter + status = sprint(io -> Pkg.status("MathOptInterface"; io)) + version = match(r"(v[0-9].[0-9]+.[0-9]+)", status)[1] + gh_moi = Documenter.Remotes.GitHub("jump-dev", "MathOptInterface.jl") + Documenter.makedocs(; + sitename = "JuMP", + authors = "The JuMP core developers and contributors", + format = Documenter.HTML(; + # See https://github.com/JuliaDocs/Documenter.jl/issues/868 + prettyurls = get(ENV, "CI", nothing) == "true", + analytics = "G-0RZ8X3D3D0", + mathengine = Documenter.MathJax2(), + collapselevel = 1, + assets = ["assets/extra_styles.css", "assets/citations.css"], + sidebar_sitename = false, + # Do no check for large pages. + size_threshold_ignore = [ + "changelog.md", + "release_notes.md", + "api/JuMP.md", + "tutorials/getting_started/getting_started_with_data_and_plotting.md", + "moi/changelog.md", + "moi/release_notes.md", + "moi/reference/models.md", + "moi/reference/standard_form.md", + "moi/submodules/Bridges/list_of_bridges.md", + "moi/submodules/Bridges/reference.md", + "moi/submodules/Utilities/reference.md", + ], + ), + modules = [JuMP, MOI], + checkdocs = :none, + doctest = _FIX ? :fix : true, + pages, + source = joinpath(@__DIR__, "src"), + build = joinpath(@__DIR__, "build"), + remotes = Dict(pkgdir(MOI) => (gh_moi, version)), + plugins = [ + DocumenterCitations.CitationBibliography( + joinpath(@__DIR__, "src", "references.bib"); + style = :authoryear, + ), + ], + ) + return +end + +# ============================================================================== +# Build the LaTeX docs (if needed) +# ============================================================================== + +function make_latex() + cd(joinpath(@__DIR__, "latex_src")) + pages = documentation_structure() + tutorial_dir = joinpath(@__DIR__, "latex_src", "tutorials") + for (root, dirs, files) in walkdir(tutorial_dir) + for dir in dirs + files = readdir(joinpath(root, dir); join = true) + for filename in filter!(endswith(".md"), files) + file = read(filename, String) + index = findfirst( + "---\n\n!!! tip\n This tutorial was generated using [Literate", + file, + ) + if index !== nothing + write(filename, file[1:(first(index)-1)]) + end + end + end + end + pop!(pages) # remove release_notes.md + pop!(pages) # remove /Extensions + pop!(pages) # remove /Solvers + moi_pages = get_moi_pages() + pop!(moi_pages[2]) # remove /MathOptInterface/release_notes.md + push!(pages, moi_pages) # Re-add /MathOptInterface + section_title, contents = pages[4] + @assert section_title == "API Reference" + # `contents` is a big list of docstrings. By default, they'll + # show up at the `\chapter` level. That's too high. + pages[4] = section_title => ["Docstrings" => contents] + Documenter.makedocs(; + sitename = "JuMP", + authors = "The JuMP core developers and contributors", + format = Documenter.LaTeX(; + platform = "tectonic", + tectonic = tectonic_jll.tectonic(), + ), + source = joinpath(@__DIR__, "latex_src"), + build = joinpath(@__DIR__, "latex_build"), + pages, + debug = true, + plugins = [ + DocumenterCitations.CitationBibliography( + joinpath(@__DIR__, "src", "references.bib"); + style = :authoryear, + ), + ], + ) + cd(@__DIR__) + return +end