diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..2145fe5f --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @vitaut diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 00000000..1b6fba6b --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,8 @@ +# Security Policy + +To report a security issue, please disclose it at [security +advisory](https://github.com/fmtlib/fmt/security/advisories/new). + +This project is maintained by a team of volunteers on a +reasonable-effort basis. As such, please give us at least 90 days to +work on a fix before public disclosure. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..8d7becf5 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,33 @@ +name: codeql + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + analyze: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + actions: read + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - uses: github/codeql-action/init@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v3.29.5 + with: + languages: c-cpp + + - name: Build + run: | + cmake -B build -DFMT_DOC=OFF -DFMT_TEST=OFF + cmake --build build --parallel + + - uses: github/codeql-action/analyze@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v3.29.5 + with: + category: /language:c-cpp diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 7aa2c011..1e2f69b3 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Add Ubuntu mirrors run: | @@ -24,7 +24,7 @@ jobs: run: | sudo apt update sudo apt install doxygen - pip install mkdocs-material==9.7.0 mkdocstrings==1.0.0 mike==2.1.3 typing_extensions==4.15.0 + pip install --require-hashes -r support/doc-requirements.txt cmake -E make_directory ${{runner.workspace}}/build # Workaround https://github.com/actions/checkout/issues/13: git config --global user.name "$(git --no-pager log --format=format:'%an' -n 1)" @@ -37,6 +37,6 @@ jobs: - name: Deploy env: KEY: "${{secrets.KEY}}" - if: env.KEY != '' && github.ref == 'refs/heads/master' + if: env.KEY != '' && github.ref == 'refs/heads/main' working-directory: ${{runner.workspace}}/fmt/build/fmt.dev run: git push https://$KEY@github.com/fmtlib/fmt.dev.git diff --git a/.github/workflows/cifuzz.yml b/.github/workflows/fuzz.yml similarity index 97% rename from .github/workflows/cifuzz.yml rename to .github/workflows/fuzz.yml index d6b94918..660e8c49 100644 --- a/.github/workflows/cifuzz.yml +++ b/.github/workflows/fuzz.yml @@ -1,11 +1,11 @@ -name: CIFuzz +name: fuzz on: [pull_request] permissions: contents: read jobs: - Fuzzing: + fuzz: runs-on: ubuntu-latest steps: - name: Build fuzzers diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 87900ac9..55879a25 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,11 +2,6 @@ name: lint on: pull_request: - paths: - - '**.h' - - '**.cc' - - '**.cmake' - - '**/CMakeLists.txt' permissions: contents: read @@ -15,13 +10,19 @@ jobs: clang-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install clang-format run: | - wget https://apt.llvm.org/llvm.sh - sudo bash ./llvm.sh 21 - sudo apt install clang-format-21 + sudo install -D -m 0644 support/llvm-snapshot.gpg.key \ + /etc/apt/keyrings/apt.llvm.org.asc + codename=$(lsb_release -cs) + echo "deb [signed-by=/etc/apt/keyrings/apt.llvm.org.asc]" \ + "https://apt.llvm.org/$codename/" \ + "llvm-toolchain-$codename-21 main" \ + | sudo tee /etc/apt/sources.list.d/llvm.list + sudo apt-get update + sudo apt-get install -y clang-format-21 - name: Run clang-format run: | @@ -32,10 +33,10 @@ jobs: cmake-format: runs-on: ubuntu-latest steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install cmake-format - run: pip install cmakelang + run: pip install --require-hashes -r support/cmake-format-requirements.txt - name: Run cmake-format run: | diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 65a07306..70260dd9 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -7,7 +7,12 @@ permissions: jobs: build: - runs-on: ubuntu-22.04 + name: >- + ${{matrix.cxx}} c++${{matrix.std}} ${{matrix.build_type}} + ${{matrix.gen}} ${{matrix.fuzz && 'Fuzz' || ''}} + ${{matrix.shared && 'Shared' || ''}} + ${{matrix.cxxflags_extra && 'Sanitize' || ''}} + runs-on: ${{ matrix.os || 'ubuntu-22.04' }} strategy: matrix: cxx: [g++-4.9, g++-11, clang++-3.6, clang++-11] @@ -16,7 +21,6 @@ jobs: shared: [""] include: - cxx: g++-4.9 - - cxx: clang++-3.6 - cxx: g++-11 build_type: Debug std: 14 @@ -31,8 +35,14 @@ jobs: - cxx: g++-13 build_type: Release std: 23 - install: sudo apt install g++-13 + os: ubuntu-24.04 shared: -DBUILD_SHARED_LIBS=ON + - cxx: g++-14 + build_type: Release + std: 23 + os: ubuntu-24.04 + gen: Ninja + - cxx: clang++-3.6 - cxx: clang++-11 build_type: Debug std: 17 @@ -48,14 +58,22 @@ jobs: - cxx: clang++-14 build_type: Debug std: 20 + cxxflags: -fsanitize=address + cxxflags_extra: -fno-sanitize-recover=all -fno-omit-frame-pointer - cxx: clang++-14 build_type: Debug std: 20 cxxflags: -stdlib=libc++ install: sudo apt install libc++-14-dev libc++abi-14-dev + - cxx: clang++-20 + build_type: Debug + std: 20 + cxxflags: -stdlib=libc++ + install: sudo apt install clang-20 libc++-20-dev libc++abi-20-dev + gen: Ninja steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set timezone run: sudo timedatectl set-timezone 'Europe/Kyiv' @@ -126,10 +144,16 @@ jobs: clang-3.6_3.6.2-3ubuntu2_amd64.deb if: ${{ matrix.cxx == 'clang++-3.6' }} - - name: Add repositories for newer GCC + - name: Install LLVM-20 run: | - sudo apt-add-repository ppa:ubuntu-toolchain-r/test - if: ${{ matrix.cxx == 'g++-13' }} + sudo install -D -m 0644 support/llvm-snapshot.gpg.key \ + /etc/apt/keyrings/apt.llvm.org.asc + codename=$(lsb_release -cs) + echo "deb [signed-by=/etc/apt/keyrings/apt.llvm.org.asc]" \ + "https://apt.llvm.org/$codename/" \ + "llvm-toolchain-$codename-20 main" \ + | sudo tee /etc/apt/sources.list.d/llvm.list + if: ${{ matrix.cxx == 'clang++-20' }} - name: Add Ubuntu mirrors run: | @@ -154,14 +178,18 @@ jobs: working-directory: ${{runner.workspace}}/build env: CXX: ${{matrix.cxx}} - CXXFLAGS: ${{matrix.cxxflags}} + CXXFLAGS: ${{matrix.cxxflags}} ${{matrix.cxxflags_extra}} run: | - cmake -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ + cmake ${{matrix.gen && '-G' || ''}} ${{matrix.gen}} \ + -DCMAKE_BUILD_TYPE=${{matrix.build_type}} \ -DCMAKE_CXX_STANDARD=${{matrix.std}} \ -DCMAKE_CXX_VISIBILITY_PRESET=hidden \ -DCMAKE_VISIBILITY_INLINES_HIDDEN=ON \ - -DFMT_DOC=OFF -DFMT_PEDANTIC=ON -DFMT_WERROR=ON \ - ${{matrix.fuzz}} ${{matrix.shared}} $GITHUB_WORKSPACE + -DFMT_DOC=OFF -DFMT_PEDANTIC=ON \ + -DFMT_WERROR=ON \ + ${{matrix.fuzz}} \ + ${{matrix.shared}} \ + $GITHUB_WORKSPACE - name: Build working-directory: ${{runner.workspace}}/build diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index ef7964ac..87fb8414 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -7,6 +7,9 @@ permissions: jobs: build: + name: >- + c++${{matrix.std }} ${{ matrix.build_type}} + ${{matrix.shared && 'Shared' || ''}} strategy: matrix: os: [macos-14] @@ -22,7 +25,7 @@ jobs: runs-on: '${{ matrix.os }}' steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set timezone run: sudo systemsetup -settimezone 'Europe/Minsk' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..30ae7b22 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,143 @@ +# Builds the release source package in CI for a draft release (typically +# created via support/release.py), uploads the zip to that release, and +# attaches a SLSA v1.0 provenance attestation generated by the OpenSSF +# slsa-github-generator. The maintainer reviews the draft (which by then has +# both the zip and *.intoto.jsonl attached) and clicks Publish to finalize. +# +# The source package includes the pre-rendered HTML documentation (doc-html/), +# so this workflow builds the docs and stages them into the source tree before +# running CPack's package_source target. +# +# This makes the provenance attest to the actual build that produced the +# artifact, rather than just attesting to a hash observed after the fact. +# +# GitHub does not fire `release: created` for draft releases, so release.py +# triggers this workflow explicitly via workflow_dispatch, passing the tag to +# attach to and the ref to build from. The `release: created` trigger is kept +# for non-draft releases created directly through the GitHub UI. + +name: release + +on: + release: + types: [created] + workflow_dispatch: + inputs: + tag_name: + description: "Release tag to attach the artifacts to (e.g. 12.2.0)" + required: true + type: string + ref: + description: "Git ref to build the source package from" + required: false + default: release + type: string + +permissions: read-all + +jobs: + build: + name: Build source package + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + hashes: ${{ steps.hash.outputs.hashes }} + package: ${{ steps.build.outputs.package }} + tag: ${{ steps.vars.outputs.tag }} + steps: + - name: Resolve tag and ref for both event types + id: vars + run: | + echo "tag=${{ github.event.release.tag_name || inputs.tag_name }}" >> "$GITHUB_OUTPUT" + echo "ref=${{ github.event.release.target_commitish || inputs.ref }}" >> "$GITHUB_OUTPUT" + + - name: Checkout the release ref + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + ref: ${{ steps.vars.outputs.ref }} + persist-credentials: false + + - name: Install the documentation toolchain + # The source zip ships the pre-rendered HTML docs (doc-html/), so the + # docs must be built before packaging. doxygen feeds the API reference; + # mkdocs plus the pinned, hash-verified deps render the site. The venv + # lives outside the checkout so it is not swept into the source zip, and + # is added to PATH so CMake's find_program(MKDOCS) and the doxygen call + # in the mkdocstrings handler both resolve in later steps. + run: | + sudo apt-get update + sudo apt-get install -y doxygen + python3 -m venv "$RUNNER_TEMP/doc-venv" + "$RUNNER_TEMP/doc-venv/bin/pip" install --quiet --upgrade pip + "$RUNNER_TEMP/doc-venv/bin/pip" install --quiet --require-hashes \ + -r support/doc-requirements.txt + echo "$RUNNER_TEMP/doc-venv/bin" >> "$GITHUB_PATH" + + - name: Build source zip via CPack + id: build + # Build the docs and stage the rendered site at the top level so the + # CPack source package includes doc-html/. The build/ tree itself is + # excluded by CPACK_SOURCE_IGNORE_FILES. + run: | + cmake -B build . + cmake --build build --target doc + cp -r build/doc-html doc-html + cmake --build build --target package_source + pkg=$(ls build/fmt-*.zip) + test -f "$pkg" + echo "package=$pkg" >> "$GITHUB_OUTPUT" + + - name: Compute base64-encoded SHA-256 subjects + id: hash + run: | + file="${{ steps.build.outputs.package }}" + subjects=$(cd "$(dirname "$file")" && sha256sum "$(basename "$file")") + echo "hashes=$(printf '%s' "$subjects" | base64 -w0)" >> "$GITHUB_OUTPUT" + + - name: Upload zip to the release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ steps.vars.outputs.tag }}" \ + "${{ steps.build.outputs.package }}" \ + --repo "${{ github.repository }}" --clobber + + # Generate the provenance only; do not let the generator upload it. With + # upload-assets, the generator creates and publishes its own release (and + # tag) to host the attestation, which duplicates our draft. Instead we + # download the attestation artifact and attach it to the draft below. + provenance: + needs: [build] + permissions: + actions: read + id-token: write + # contents: write is required because the generator's (skipped) + # upload-assets job declares it, and a reusable workflow's job + # permissions may not exceed the caller's, or the run fails at startup. + contents: write + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + with: + base64-subjects: ${{ needs.build.outputs.hashes }} + provenance-name: "fmt-${{ needs.build.outputs.tag }}.intoto.jsonl" + upload-assets: false + + attach-provenance: + name: Attach provenance to the release + needs: [build, provenance] + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Download the provenance attestation + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: ${{ needs.provenance.outputs.provenance-name }} + + - name: Attach provenance to the draft release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release upload "${{ needs.build.outputs.tag }}" \ + "${{ needs.provenance.outputs.provenance-name }}" \ + --repo "${{ github.repository }}" --clobber diff --git a/.github/workflows/scorecard.yml b/.github/workflows/supply-chain.yml similarity index 50% rename from .github/workflows/scorecard.yml rename to .github/workflows/supply-chain.yml index 7d389b2a..0c30e0f8 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/supply-chain.yml @@ -1,9 +1,6 @@ -# This workflow uses actions that are not certified by GitHub. They are provided -# by a third-party and are governed by separate terms of service, privacy -# policy, and support documentation. - -name: Scorecard supply-chain security +name: supply-chain on: + workflow_dispatch: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: @@ -12,14 +9,13 @@ on: schedule: - cron: '26 14 * * 5' push: - branches: [ "master" ] + branches: [main] # Declare default permissions as read only. permissions: read-all jobs: - analysis: - name: Scorecard analysis + scorecard: runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. @@ -28,20 +24,15 @@ jobs: id-token: write steps: - - name: "Checkout code" - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - - name: "Run analysis" + - name: Run analysis uses: ossf/scorecard-action@05b42c624433fc40578a4040d5cf5e36ddca8cde # v2.4.2 with: results_file: results.sarif results_format: sarif - # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: - # - you want to enable the Branch-Protection check on a *public* repository, or - # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action#authentication-with-pat. - # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers @@ -49,17 +40,8 @@ jobs: # - See https://github.com/ossf/scorecard-action#publishing-results. publish_results: true - # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF - # format to the repository Actions tab. - - name: "Upload artifact" - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 - with: - name: SARIF file - path: results.sarif - retention-days: 5 - # Upload the results to GitHub's code scanning dashboard. - - name: "Upload to code-scanning" - uses: github/codeql-action/upload-sarif@b20883b0cd1f46c72ae0ba6d1090936928f9fa30 # v3.29.5 + - name: Upload to code-scanning + uses: github/codeql-action/upload-sarif@87557b9c84dde89fdd9b10e88954ac2f4248e463 # v3.29.5 with: sarif_file: results.sarif diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index cabd2833..b439bcaa 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -7,6 +7,9 @@ permissions: jobs: build: + name: >- + ${{matrix.platform}} ${{matrix.toolset}} c++${{matrix.standard}} + ${{matrix.build_type}} ${{matrix.shared && 'Shared' || ''}} runs-on: ${{matrix.os}} strategy: matrix: @@ -32,7 +35,7 @@ jobs: standard: 20 steps: - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Set timezone run: tzutil /s "FLE Standard Time" @@ -79,7 +82,7 @@ jobs: release: false msystem: ${{matrix.sys}} pacboy: cc:p cmake:p ninja:p lld:p - - uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Configure run: cmake -B ../build -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Debug env: { LDFLAGS: -fuse-ld=lld } diff --git a/.gitignore b/.gitignore index 25924b1e..235a2f56 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.so* *.xcodeproj *~ +*.gradle/ .vscode/ .cache/ .vs/ diff --git a/CMakeLists.txt b/CMakeLists.txt index cc47c96e..9fd7662d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,7 +63,7 @@ if (CMAKE_VERSION VERSION_GREATER_EQUAL 3.28 AND CMAKE_CXX_STANDARD OUTPUT_VARIABLE NINJA_VERSION) if (NINJA_VERSION VERSION_GREATER_EQUAL 1.11) if ((CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION - GREATER_EQUAL 14) + GREATER_EQUAL 15) OR (CMAKE_CXX_COMPILER_ID STREQUAL "Clang" AND CMAKE_CXX_COMPILER_VERSION GREATER_EQUAL 16) OR (CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND MSVC_VERSION @@ -256,11 +256,13 @@ function (setup_target target kind) target_compile_definitions(${target} ${kind} FMT_UNICODE=0) endif () - set_target_properties( - ${target} - PROPERTIES VERSION ${FMT_VERSION} - SOVERSION ${CPACK_PACKAGE_VERSION_MAJOR} - DEBUG_POSTFIX "${FMT_DEBUG_POSTFIX}") + if (NOT "${kind}" STREQUAL "INTERFACE") + set_target_properties( + ${target} + PROPERTIES VERSION ${FMT_VERSION} + SOVERSION ${CPACK_PACKAGE_VERSION_MAJOR} + DEBUG_POSTFIX "${FMT_DEBUG_POSTFIX}") + endif () endfunction () set(FMT_HEADERS) @@ -417,8 +419,19 @@ target_compile_definitions(fmt-header-only INTERFACE FMT_HEADER_ONLY=1) target_compile_features(fmt-header-only INTERFACE cxx_std_11) setup_target(fmt-header-only INTERFACE) -add_library(fmt-c STATIC src/fmt-c.cc) +add_library(fmt-c src/fmt-c.cc) target_compile_features(fmt-c INTERFACE c_std_11) +if (BUILD_SHARED_LIBS) + target_compile_definitions( + fmt-c + PRIVATE FMT_LIB_EXPORT + INTERFACE FMT_SHARED) +endif () +set_target_properties( + fmt-c + PROPERTIES VERSION ${FMT_VERSION} + SOVERSION ${CPACK_PACKAGE_VERSION_MAJOR} + DEBUG_POSTFIX "${FMT_DEBUG_POSTFIX}") if (MSVC) target_compile_options(fmt-c PUBLIC /Zc:preprocessor) endif () @@ -428,6 +441,16 @@ add_library(fmt::fmt-c ALIAS fmt-c) set_target_properties(fmt-c PROPERTIES PUBLIC_HEADER include/fmt/fmt-c.h) # Install targets. +set(targets_export_name fmt-targets) +set(INSTALL_TARGETS fmt fmt-header-only fmt-c) + +if (FMT_MODULE) + list(APPEND INSTALL_TARGETS fmt-module) + if (FMT_USE_CMAKE_MODULES) + set(INSTALL_FILE_SET FILE_SET fmt DESTINATION ${FMT_INC_DIR}/fmt) + endif () +endif () + if (FMT_INSTALL) include(CMakePackageConfigHelpers) set_verbose( @@ -441,7 +464,6 @@ if (FMT_INSTALL) set(version_config ${PROJECT_BINARY_DIR}/fmt-config-version.cmake) set(project_config ${PROJECT_BINARY_DIR}/fmt-config.cmake) set(pkgconfig ${PROJECT_BINARY_DIR}/fmt.pc) - set(targets_export_name fmt-targets) set_verbose( FMT_LIB_DIR ${CMAKE_INSTALL_LIBDIR} CACHE STRING @@ -472,15 +494,6 @@ if (FMT_INSTALL) ${PROJECT_SOURCE_DIR}/support/cmake/fmt-config.cmake.in ${project_config} INSTALL_DESTINATION ${FMT_CMAKE_DIR}) - set(INSTALL_TARGETS fmt fmt-header-only fmt-c) - - if (FMT_MODULE) - list(APPEND INSTALL_TARGETS fmt-module) - if (FMT_USE_CMAKE_MODULES) - set(INSTALL_FILE_SET FILE_SET fmt DESTINATION ${FMT_INC_DIR}/fmt) - endif () - endif () - # Install the library and headers. install( TARGETS ${INSTALL_TARGETS} @@ -491,13 +504,6 @@ if (FMT_INSTALL) PUBLIC_HEADER DESTINATION ${FMT_INC_DIR}/fmt RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} ${INSTALL_FILE_SET}) - # Use a namespace because CMake provides better diagnostics for namespaced - # imported targets. - export( - TARGETS ${INSTALL_TARGETS} - NAMESPACE fmt:: - FILE ${PROJECT_BINARY_DIR}/${targets_export_name}.cmake) - # Install version, config and target files. install( FILES ${project_config} ${version_config} @@ -515,6 +521,15 @@ if (FMT_INSTALL) COMPONENT fmt_core) endif () +# Generate an export file in the build tree so that fmt can be used by +# add_subdirectory() or FetchContent without being installed. Use a +# namespace because CMake provides better diagnostics for namespaced +# imported targets. +export( + TARGETS ${INSTALL_TARGETS} + NAMESPACE fmt:: + FILE ${PROJECT_BINARY_DIR}/${targets_export_name}.cmake) + function (add_doc_target) find_program(DOXYGEN doxygen PATHS "$ENV{ProgramFiles}/doxygen/bin" "$ENV{ProgramFiles\(x86\)}/doxygen/bin") @@ -582,7 +597,9 @@ if (FMT_MASTER_PROJECT AND EXISTS ${gitignore}) string(REPLACE "*" ".*" line "${line}") set(ignored_files ${ignored_files} "${line}$" "${line}/") endforeach () - set(ignored_files ${ignored_files} /.git /build/doxyxml .vagrant) + # Exclude the whole build tree: the .gitignore-derived `build/` pattern does + # not actually prune the directory or its contents from the source package. + set(ignored_files ${ignored_files} /.git "/build($|/)" .vagrant) set(CPACK_SOURCE_GENERATOR ZIP) set(CPACK_SOURCE_IGNORE_FILES ${ignored_files}) diff --git a/ChangeLog.md b/ChangeLog.md index e96f9df8..d132266d 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -1,3 +1,226 @@ +# 12.2.0 - 2026-06-16 + +- Added a C11 API that brings fast, type-safe formatting to C. The new + `fmt-c` library and `fmt/fmt-c.h` header use `_Generic` to dispatch on + argument types and outperform `printf`/`sprintf`. For example: + + ```c++ + #include + + fmt_print(stdout, "The answer is {}.\n", 42); + ``` + + (https://github.com/fmtlib/fmt/issues/4663, + https://github.com/fmtlib/fmt/pull/4671, + https://github.com/fmtlib/fmt/pull/4696, + https://github.com/fmtlib/fmt/issues/4693, + https://github.com/fmtlib/fmt/pull/4694, + https://github.com/fmtlib/fmt/pull/4712, + https://github.com/fmtlib/fmt/pull/4789). + Thanks @Soumik15630m, @Ferdi265 and @localspook. + +- Added a separate `fmt::fmt-module` CMake target for C++20 modules and a + CI workflow that exercises module-based builds + (https://github.com/fmtlib/fmt/issues/4684, + https://github.com/fmtlib/fmt/pull/4685, + https://github.com/fmtlib/fmt/issues/4707, + https://github.com/fmtlib/fmt/pull/4708, + https://github.com/fmtlib/fmt/pull/4702, + https://github.com/fmtlib/fmt/pull/4709). + Thanks @MathewBensonCode. + +- Enabled the full Dragonbox lookup cache by default for floating-point + formatting unless optimizing for binary size (`__OPTIMIZE_SIZE__`), + giving a ~10–25% speedup. Thanks Matthias Kretz for the suggestion. + Average time per `double` on Apple M1 Pro (clang 17, random digits, + smaller is better) measured with + [dtoa-benchmark](https://github.com/fmtlib/dtoa-benchmark): + + | Method | Time (ns) | + | ----------------------- | --------: | + | fmt (full) | 22.07 | + | fmt (compact) | 29.55 | + | ryu | 35.21 | + | double-conversion | 81.81 | + | `sprintf` | 726.27 | + | `ostringstream` | 864.34 | + +- Improved integer formatting performance by ~3% + (https://github.com/fmtlib/fmt/pull/4630). Thanks @user202729. + +- Optimized formatting into back-insert iterators by using bulk container + append/insert methods (e.g. on `std::vector` and custom string + types) (https://github.com/fmtlib/fmt/pull/4679). Thanks @user202729. + +- Reduced binary size of debug builds (~200k to ~85k in the bloat test) and + improved compile speed when `consteval` is unavailable. + +- Made path formatting lossless, preserving ill-formed UTF-16 + sequences when converting `std::filesystem::path` to a narrow string. + +- Added support for formatting `std::unexpected` + (https://github.com/fmtlib/fmt/pull/4675). Thanks @17steen. + +- Added overloads of `fmt::println` that take a `fmt::text_style` + (https://github.com/fmtlib/fmt/pull/4782). Thanks @ahoarau. + +- Added support for positional arguments as width and precision specifiers + in `fmt::printf` (https://github.com/fmtlib/fmt/pull/4643). + Thanks @KareemOtoum. + +- Made `FMT_STRING` a no-op when `FMT_USE_CONSTEVAL` is enabled, since the + consteval format-string constructor already provides compile-time + validation + (https://github.com/fmtlib/fmt/issues/4611, + https://github.com/fmtlib/fmt/pull/4612). Thanks @friedkeenan. + +- Promoted `fmt::detail::named_arg` to the public API as `fmt::named_arg` and + deprecated the detail alias + (https://github.com/fmtlib/fmt/issues/4683, + https://github.com/fmtlib/fmt/pull/4687). Thanks @TPPPP72. + +- Moved the `std::byte` formatter from `fmt/format.h` to `fmt/std.h`. + +- Provided a default definition for `fmt::is_contiguous` + (https://github.com/fmtlib/fmt/pull/4731, + https://github.com/fmtlib/fmt/pull/4770). Thanks @user202729 and @phprus. + +- Added the `FMT_USE_FLOCKFILE` macro to disable the use of `flockfile` + (https://github.com/fmtlib/fmt/issues/4646, + https://github.com/fmtlib/fmt/pull/4666). Thanks @mvastola. + +- Added `include_guard(GLOBAL)` so that {fmt} can be used in multiple + submodules of the same project + (https://github.com/fmtlib/fmt/pull/4672). Thanks @torsten48. + +- Improved `constexpr` support + (https://github.com/fmtlib/fmt/pull/4659, + https://github.com/fmtlib/fmt/pull/4591). + Thanks @elbeno and @17steen. + +- Deprecated the implicit conversion from `fmt::format_string` and + `fmt::basic_fstring` to `string_view` to align with `std::format_string`; + use `format_string::get()` instead. + +- Opted out `std::complex` from tuple formatting so that the dedicated + `std::complex` formatter is always used. + +- Removed the `fmt::say` function. + +- Deprecated the `std::initializer_list` overload of `fmt::join` and the array + overload of `fmt::vformat_to`. + +- Made the `` header equivalent to `` by + default. Code that relied on `` pulling in `` + must now either include `` directly or define + `FMT_DEPRECATED_HEAVY_CORE` to opt back in. + +- Improved `wchar_t` support: `fmt::join` now accepts `wchar_t` and other + non-`char` separators, and `fmt::format_to_n` now works with `fmt::runtime` + on `wchar_t` + (https://github.com/fmtlib/fmt/pull/4686, + https://github.com/fmtlib/fmt/issues/4714, + https://github.com/fmtlib/fmt/pull/4715). + Thanks @Yancey2023 and @sunmy2019. + +- Fixed formatting of `std::tm` with a null `tm_zone` + (https://github.com/fmtlib/fmt/pull/4790). Thanks @Carmel0. + +- Fixed compile-time formatting in `fmt/ranges.h`, `fmt/style.h` and + `fmt/std.h` (https://github.com/fmtlib/fmt/pull/4759). Thanks @j4niwzis. + +- Fixed an ambiguity between `formatter>` in `fmt/std.h` + and `fmt/ranges.h` on C++26 (P3168R2) + (https://github.com/fmtlib/fmt/pull/4761). Thanks @phprus. + +- Fixed a GCC PCH breakage triggered by a scoped `#pragma GCC optimize`. + +- Fixed a TSAN false positive in the locale handling code + (https://github.com/fmtlib/fmt/issues/4755). + +- Fixed compile-time format string checks truncating string literals at + an embedded null byte + (https://github.com/fmtlib/fmt/pull/4732). Thanks @user202729. + +- Fixed out-of-bounds reads in `printf` formatting + (https://github.com/fmtlib/fmt/issues/4741, + https://github.com/fmtlib/fmt/pull/4742, + https://github.com/fmtlib/fmt/pull/4800). + Thanks @Algunenano and @aizu-m. + +- Fixed the return type of the `f(un)lockfile` wrappers on Windows + (https://github.com/fmtlib/fmt/pull/4739). Thanks @mvastola. + +- Worked around a CUDA issue when handling UTF-32 literals + (https://github.com/fmtlib/fmt/pull/4719). Thanks @Cazadorro. + +- Fixed missing named-argument validation for compiled format strings + (https://github.com/fmtlib/fmt/pull/4638). Thanks @JaeheonShim. + +- Fixed `fmt::format_to_n` in `` failing to compile when + `` is not transitively included + (https://github.com/fmtlib/fmt/issues/4615). + +- Fixed handling of pointers in format string compilation with + `FMT_BUILTIN_TYPES=0`. + +- Stopped assuming nul termination of the format string in `fmt::printf`. + Thanks @ZUENS2020 for reporting. + +- Fixed a build error when locale support is disabled + (https://github.com/fmtlib/fmt/pull/4627). Thanks @marcel-behlau-elfin. + +- Fixed a fallback range formatter for types with a `container_type` member + (https://github.com/fmtlib/fmt/issues/4123, + https://github.com/fmtlib/fmt/pull/4660). Thanks @Soumik15630m. + +- Fixed C++20 concept detection + (https://github.com/fmtlib/fmt/pull/4653). Thanks @tearfur. + +- Fixed a clang compilation failure + (https://github.com/fmtlib/fmt/pull/4718). Thanks @mccakit. + +- Fixed various MSVC warnings, including C4305 and conversion warnings on + x86 (https://github.com/fmtlib/fmt/pull/4668, + https://github.com/fmtlib/fmt/pull/4594). + Thanks @kanren3 and @blizzard4591. + +- Updated the Android Gradle Plugin to 9.x + (https://github.com/fmtlib/fmt/issues/4651, + https://github.com/fmtlib/fmt/pull/4658). Thanks @Soumik15630m. + +- Made various code, build and test improvements + (https://github.com/fmtlib/fmt/pull/4625, + https://github.com/fmtlib/fmt/pull/4639, + https://github.com/fmtlib/fmt/pull/4644, + https://github.com/fmtlib/fmt/pull/4656, + https://github.com/fmtlib/fmt/pull/4680, + https://github.com/fmtlib/fmt/pull/4681, + https://github.com/fmtlib/fmt/pull/4704, + https://github.com/fmtlib/fmt/pull/4710, + https://github.com/fmtlib/fmt/pull/4713, + https://github.com/fmtlib/fmt/pull/4729, + https://github.com/fmtlib/fmt/pull/4751, + https://github.com/fmtlib/fmt/pull/4758, + https://github.com/fmtlib/fmt/pull/4799). + Thanks @ZephyrLykos, @togunchan, @kagancansit, @BerndPetrovitsch, + @Skylion007, @st0rmbtw, @localspook and @EXtremeExploit. + +- Improved documentation, including a rewrite of the format string syntax, + better handling of doxygen tags, documenting `output_file`, fixing CSS so + that whitespace is displayed properly, and various smaller fixes + (https://github.com/fmtlib/fmt/pull/4622, + https://github.com/fmtlib/fmt/pull/4626, + https://github.com/fmtlib/fmt/pull/4631, + https://github.com/fmtlib/fmt/pull/4667, + https://github.com/fmtlib/fmt/pull/4616, + https://github.com/fmtlib/fmt/pull/4748). + Thanks @heavywatal, @ZephyrLykos, @user202729, @ssszcmawo, @bigmoonbit + and @Powerbyte7. + +- Added building of release artifacts and SLSA provenance in CI, added a + CodeQL workflow, and added the security policy in `.github/SECURITY.md`. + # 12.1.0 - 2025-10-29 - Optimized `buffer::append`, resulting in up to ~16% improvement on spdlog diff --git a/README.md b/README.md index 040d17cb..d2c1af9a 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,25 @@ {fmt} -[![image](https://github.com/fmtlib/fmt/actions/workflows/linux.yml/badge.svg?branch=master)](https://github.com/fmtlib/fmt/actions?query=workflow%3Alinux) -[![image](https://github.com/fmtlib/fmt/actions/workflows/macos.yml/badge.svg?branch=master)](https://github.com/fmtlib/fmt/actions?query=workflow%3Amacos) -[![image](https://github.com/fmtlib/fmt/actions/workflows/windows.yml/badge.svg?branch=master)](https://github.com/fmtlib/fmt/actions?query=workflow%3Awindows) -[![fmt is continuously fuzzed at oss-fuzz](https://oss-fuzz-build-logs.storage.googleapis.com/badges/fmt.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?\%0Acolspec=ID%20Type%20Component%20Status%20Proj%20Reported%20Owner%20\%0ASummary&q=proj%3Dfmt&can=1) -[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8880/badge)](https://www.bestpractices.dev/projects/8880) -[![image](https://api.securityscorecards.dev/projects/github.com/fmtlib/fmt/badge)](https://securityscorecards.dev/viewer/?uri=github.com/fmtlib/fmt) -[![Ask questions at StackOverflow with the tag fmt](https://img.shields.io/badge/stackoverflow-fmt-blue.svg)](https://stackoverflow.com/questions/tagged/fmt) +[![image](https://github.com/fmtlib/fmt/actions/workflows/linux.yml/badge.svg?branch=master)]( +https://github.com/fmtlib/fmt/actions?query=workflow%3Alinux) +[![image](https://github.com/fmtlib/fmt/actions/workflows/macos.yml/badge.svg?branch=master)]( +https://github.com/fmtlib/fmt/actions?query=workflow%3Amacos) +[![image](https://github.com/fmtlib/fmt/actions/workflows/windows.yml/badge.svg?branch=master)]( +https://github.com/fmtlib/fmt/actions?query=workflow%3Awindows) +[![fmt is continuously fuzzed at oss-fuzz](https://oss-fuzz-build-logs.storage.googleapis.com/badges/fmt.svg)]( +https://issues.oss-fuzz.com/issues?q=title:fmt%20cc:victor.zverovich@gmail.com) +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8880/badge)]( +https://www.bestpractices.dev/projects/8880) +[![image](https://api.securityscorecards.dev/projects/github.com/fmtlib/fmt/badge)]( +https://securityscorecards.dev/viewer/?uri=github.com/fmtlib/fmt) +[![Ask questions at StackOverflow with the tag fmt]( +https://img.shields.io/badge/stackoverflow-fmt-blue.svg)](https://stackoverflow.com/questions/tagged/fmt) +[![Support Ukraine]( +https://img.shields.io/badge/Support-Ukraine-005BBB?labelColor=FFD500)](https://novaukraine.org/) **{fmt}** is an open-source formatting library providing a fast and safe alternative to C stdio and C++ iostreams. -If you like this project, please consider donating to one of the funds -that help victims of the war in Ukraine: . - [Documentation](https://fmt.dev) [Cheat Sheets](https://hackingcpp.com/cpp/libs/fmt.html) @@ -38,7 +44,7 @@ Try {fmt} in [Compiler Explorer](https://godbolt.org/z/8Mx1EW73v). [Dragonbox](https://github.com/jk-jeon/dragonbox) algorithm - Portable Unicode support - Safe [printf - implementation](https://fmt.dev/latest/api/#printf-formatting) + implementation](https://fmt.dev/latest/api/#printf-api) including the POSIX extension for positional arguments - Extensibility: [support for user-defined types](https://fmt.dev/latest/api/#formatting-user-defined-types) @@ -46,7 +52,7 @@ Try {fmt} in [Compiler Explorer](https://godbolt.org/z/8Mx1EW73v). implementations of `(s)printf`, iostreams, `to_string` and `to_chars`, see [Speed tests](#speed-tests) and [Converting a hundred million integers to strings per - second](http://www.zverovich.net/2020/06/13/fast-int-to-string-revisited.html) + second](https://vitaut.net/posts/2020/fast-int-to-string-revisited/) - Small code size both in terms of source code with the minimum configuration consisting of just three files, `base.h`, `format.h` and `format-inl.h`, and compiled code; see [Compile time and code @@ -151,7 +157,7 @@ int main() { ``` This can be [up to 9 times faster than `fprintf`]( -http://www.zverovich.net/2020/08/04/optimal-file-buffer-size.html). +https://vitaut.net/posts/2020/optimal-file-buffer-size/). **Print with colors and text styles** @@ -170,7 +176,7 @@ int main() { Output on a modern terminal with Unicode support: -![image](https://github.com/fmtlib/fmt/assets/%0A576385/2a93c904-d6fa-4aa6-b453-2618e1c327d7) +![image](https://github.com/fmtlib/fmt/assets/576385/2a93c904-d6fa-4aa6-b453-2618e1c327d7) # Benchmarks @@ -345,7 +351,7 @@ converts to `std::print`.) cluster proxy - [redpanda](https://vectorized.io/redpanda): a 10x faster Kafka® replacement for mission-critical systems written in C++ -- [rpclib](http://rpclib.net/): a modern C++ msgpack-RPC server and +- [rpclib](https://github.com/rpclib/rpclib): a modern C++ msgpack-RPC server and client library - [Salesforce Analytics Cloud](https://www.salesforce.com/analytics-cloud/overview/): @@ -353,7 +359,7 @@ converts to `std::print`.) - [Scylla](https://www.scylladb.com/): a Cassandra-compatible NoSQL data store that can handle 1 million transactions per second on a single server -- [Seastar](http://www.seastar-project.org/): an advanced, open-source +- [Seastar](https://seastar.io/): an advanced, open-source C++ framework for high-performance server applications on modern hardware - [spdlog](https://github.com/gabime/spdlog): super fast C++ logging @@ -374,98 +380,6 @@ If you are aware of other projects using this library, please let me know by [email](mailto:victor.zverovich@gmail.com) or by submitting an [issue](https://github.com/fmtlib/fmt/issues). -# Motivation - -So why yet another formatting library? - -There are plenty of methods for doing this task, from standard ones like -the printf family of function and iostreams to Boost Format and -FastFormat libraries. The reason for creating a new library is that -every existing solution that I found either had serious issues or -didn\'t provide all the features I needed. - -## printf - -The good thing about `printf` is that it is pretty fast and readily -available being a part of the C standard library. The main drawback is -that it doesn\'t support user-defined types. `printf` also has safety -issues although they are somewhat mitigated with [\_\_attribute\_\_ -((format (printf, -\...))](https://gcc.gnu.org/onlinedocs/gcc/Common-Attributes.html) in -GCC. There is a POSIX extension that adds positional arguments required -for -[i18n](https://en.wikipedia.org/wiki/Internationalization_and_localization) -to `printf` but it is not a part of C99 and may not be available on some -platforms. - -## iostreams - -The main issue with iostreams is best illustrated with an example: - -``` c++ -std::cout << std::setprecision(2) << std::fixed << 1.23456 << "\n"; -``` - -which is a lot of typing compared to printf: - -``` c++ -printf("%.2f\n", 1.23456); -``` - -Matthew Wilson, the author of FastFormat, called this \"chevron hell\". -iostreams don\'t support positional arguments by design. - -The good part is that iostreams support user-defined types and are safe -although error handling is awkward. - -## Boost Format - -This is a very powerful library that supports both `printf`-like format -strings and positional arguments. Its main drawback is performance. -According to various benchmarks, it is much slower than other methods -considered here. Boost Format also has excessive build times and severe -code bloat issues (see [Benchmarks](#benchmarks)). - -## FastFormat - -This is an interesting library that is fast, safe and has positional -arguments. However, it has significant limitations, citing its author: - -> Three features that have no hope of being accommodated within the -> current design are: -> -> - Leading zeros (or any other non-space padding) -> - Octal/hexadecimal encoding -> - Runtime width/alignment specification - -It is also quite big and has a heavy dependency, on STLSoft, which might be -too restrictive for use in some projects. - -## Boost Spirit.Karma - -This is not a formatting library but I decided to include it here for -completeness. As iostreams, it suffers from the problem of mixing -verbatim text with arguments. The library is pretty fast, but slower on -integer formatting than `fmt::format_to` with format string compilation -on Karma\'s own benchmark, see [Converting a hundred million integers to -strings per -second](http://www.zverovich.net/2020/06/13/fast-int-to-string-revisited.html). - -# License - -{fmt} is distributed under the MIT -[license](https://github.com/fmtlib/fmt/blob/master/LICENSE). - -# Documentation License - -The [Format String Syntax](https://fmt.dev/latest/syntax/) section -in the documentation is based on the one from Python [string module -documentation](https://docs.python.org/3/library/string.html#module-string). -For this reason, the documentation is distributed under the Python -Software Foundation license available in -[doc/python-license.txt](https://raw.github.com/fmtlib/fmt/master/doc/python-license.txt). -It only applies if you distribute the documentation of {fmt}. - # Maintainers The {fmt} library is maintained by Victor Zverovich @@ -475,12 +389,3 @@ people. See [Releases](https://github.com/fmtlib/fmt/releases) for some of the names. Let us know if your contribution is not listed or mentioned incorrectly and we\'ll make it right. - -# Security Policy - -To report a security issue, please disclose it at [security -advisory](https://github.com/fmtlib/fmt/security/advisories/new). - -This project is maintained by a team of volunteers on a -reasonable-effort basis. As such, please give us at least *90* days to -work on a fix before public exposure. diff --git a/doc/index.md b/doc/index.md index e312c398..217ade6b 100644 --- a/doc/index.md +++ b/doc/index.md @@ -130,7 +130,6 @@ hide: Where possible, the output of formatting functions is consistent across platforms.

-

diff --git a/doc/python-license.txt b/doc/python-license.txt deleted file mode 100644 index 88eed1f9..00000000 --- a/doc/python-license.txt +++ /dev/null @@ -1,290 +0,0 @@ -A. HISTORY OF THE SOFTWARE -========================== - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations (now Zope -Corporation, see http://www.zope.com). In 2001, the Python Software -Foundation (PSF, see http://www.python.org/psf/) was formed, a -non-profit organization created specifically to own Python-related -Intellectual Property. Zope Corporation is a sponsoring member of -the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases. - - Release Derived Year Owner GPL- - from compatible? (1) - - 0.9.0 thru 1.2 1991-1995 CWI yes - 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes - 1.6 1.5.2 2000 CNRI no - 2.0 1.6 2000 BeOpen.com no - 1.6.1 1.6 2001 CNRI yes (2) - 2.1 2.0+1.6.1 2001 PSF no - 2.0.1 2.0+1.6.1 2001 PSF yes - 2.1.1 2.1+2.0.1 2001 PSF yes - 2.2 2.1.1 2001 PSF yes - 2.1.2 2.1.1 2002 PSF yes - 2.1.3 2.1.2 2002 PSF yes - 2.2.1 2.2 2002 PSF yes - 2.2.2 2.2.1 2002 PSF yes - 2.2.3 2.2.2 2003 PSF yes - 2.3 2.2.2 2002-2003 PSF yes - 2.3.1 2.3 2002-2003 PSF yes - 2.3.2 2.3.1 2002-2003 PSF yes - 2.3.3 2.3.2 2002-2003 PSF yes - 2.3.4 2.3.3 2004 PSF yes - 2.3.5 2.3.4 2005 PSF yes - 2.4 2.3 2004 PSF yes - 2.4.1 2.4 2005 PSF yes - 2.4.2 2.4.1 2005 PSF yes - 2.4.3 2.4.2 2006 PSF yes - 2.4.4 2.4.3 2006 PSF yes - 2.5 2.4 2006 PSF yes - 2.5.1 2.5 2007 PSF yes - 2.5.2 2.5.1 2008 PSF yes - 2.5.3 2.5.2 2008 PSF yes - 2.6 2.5 2008 PSF yes - 2.6.1 2.6 2008 PSF yes - 2.6.2 2.6.1 2009 PSF yes - 2.6.3 2.6.2 2009 PSF yes - 2.6.4 2.6.3 2009 PSF yes - 2.6.5 2.6.4 2010 PSF yes - 3.0 2.6 2008 PSF yes - 3.0.1 3.0 2009 PSF yes - 3.1 3.0.1 2009 PSF yes - 3.1.1 3.1 2009 PSF yes - 3.1.2 3.1.1 2010 PSF yes - 3.1.3 3.1.2 2010 PSF yes - 3.1.4 3.1.3 2011 PSF yes - 3.2 3.1 2011 PSF yes - 3.2.1 3.2 2011 PSF yes - 3.2.2 3.2.1 2011 PSF yes - 3.2.3 3.2.2 2012 PSF yes - 3.3.0 3.2 2012 PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, - because its license has a choice of law clause. According to - CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 - is "not incompatible" with the GPL. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012 Python Software Foundation; All Rights Reserved" are retained in Python -alone or in any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 -------------------------------------------- - -BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 - -1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an -office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the -Individual or Organization ("Licensee") accessing and otherwise using -this software in source or binary form and its associated -documentation ("the Software"). - -2. Subject to the terms and conditions of this BeOpen Python License -Agreement, BeOpen hereby grants Licensee a non-exclusive, -royalty-free, world-wide license to reproduce, analyze, test, perform -and/or display publicly, prepare derivative works, distribute, and -otherwise use the Software alone or in any derivative version, -provided, however, that the BeOpen Python License is retained in the -Software, alone or in any derivative version prepared by Licensee. - -3. BeOpen is making the Software available to Licensee on an "AS IS" -basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE -SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS -AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY -DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -5. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -6. This License Agreement shall be governed by and interpreted in all -respects by the law of the State of California, excluding conflict of -law provisions. Nothing in this License Agreement shall be deemed to -create any relationship of agency, partnership, or joint venture -between BeOpen and Licensee. This License Agreement does not grant -permission to use BeOpen trademarks or trade names in a trademark -sense to endorse or promote products or services of Licensee, or any -third party. As an exception, the "BeOpen Python" logos available at -http://www.pythonlabs.com/logos.html may be used according to the -permissions granted on that web page. - -7. By copying, installing or otherwise using the software, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. - - -CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 ---------------------------------------- - -1. This LICENSE AGREEMENT is between the Corporation for National -Research Initiatives, having an office at 1895 Preston White Drive, -Reston, VA 20191 ("CNRI"), and the Individual or Organization -("Licensee") accessing and otherwise using Python 1.6.1 software in -source or binary form and its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, CNRI -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python 1.6.1 -alone or in any derivative version, provided, however, that CNRI's -License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) -1995-2001 Corporation for National Research Initiatives; All Rights -Reserved" are retained in Python 1.6.1 alone or in any derivative -version prepared by Licensee. Alternately, in lieu of CNRI's License -Agreement, Licensee may substitute the following text (omitting the -quotes): "Python 1.6.1 is made available subject to the terms and -conditions in CNRI's License Agreement. This Agreement together with -Python 1.6.1 may be located on the Internet using the following -unique, persistent identifier (known as a handle): 1895.22/1013. This -Agreement may also be obtained from a proxy server on the Internet -using the following URL: http://hdl.handle.net/1895.22/1013". - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python 1.6.1 or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python 1.6.1. - -4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" -basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. This License Agreement shall be governed by the federal -intellectual property law of the United States, including without -limitation the federal copyright law, and, to the extent such -U.S. federal law does not apply, by the law of the Commonwealth of -Virginia, excluding Virginia's conflict of law provisions. -Notwithstanding the foregoing, with regard to derivative works based -on Python 1.6.1 that incorporate non-separable material that was -previously distributed under the GNU General Public License (GPL), the -law of the Commonwealth of Virginia shall govern this License -Agreement only as to issues arising under or with respect to -Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this -License Agreement shall be deemed to create any relationship of -agency, partnership, or joint venture between CNRI and Licensee. This -License Agreement does not grant permission to use CNRI trademarks or -trade name in a trademark sense to endorse or promote products or -services of Licensee, or any third party. - -8. By clicking on the "ACCEPT" button where indicated, or by copying, -installing or otherwise using Python 1.6.1, Licensee agrees to be -bound by the terms and conditions of this License Agreement. - - ACCEPT - - -CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 --------------------------------------------------- - -Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, -The Netherlands. All rights reserved. - -Permission to use, copy, modify, and distribute this software and its -documentation for any purpose and without fee is hereby granted, -provided that the above copyright notice appear in all copies and that -both that copyright notice and this permission notice appear in -supporting documentation, and that the name of Stichting Mathematisch -Centrum or CWI not be used in advertising or publicity pertaining to -distribution of the software without specific, written prior -permission. - -STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO -THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND -FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE -FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES -WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN -ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT -OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/doc/syntax.md b/doc/syntax.md index ac512823..9df3cd93 100644 --- a/doc/syntax.md +++ b/doc/syntax.md @@ -1,14 +1,15 @@ # Format String Syntax -Formatting functions such as [`fmt::format`](api.md#format) and [`fmt::print`]( -api.md#print) use the same format string syntax described in this section. +The formatting functions in this library — most notably +[`fmt::format`](api.md#format) and [`fmt::print`](api.md#print) — accept +format strings written in the syntax described here. -Format strings contain "replacement fields" surrounded by curly braces `{}`. -Anything that is not contained in braces is considered literal text, which is -copied unchanged to the output. If you need to include a brace character in -the literal text, it can be escaped by doubling: `{{` and `}}`. +A format string is plain text with embedded *replacement fields* delimited by +the braces `{` and `}`. Characters outside of any replacement field are +copied to the output unchanged. To emit a literal brace, double it: `{{` +yields a single `{` in the output, and `}}` yields a single `}`. -The grammar for a replacement field is as follows: +A replacement field is described by the grammar below.

 
-In less formal terms, the replacement field can start with an *arg_id* that -specifies the argument whose value is to be formatted and inserted into the -output instead of the replacement field. The *arg_id* is optionally followed -by a *format_spec*, which is preceded by a colon `':'`. These specify a -non-default format for the replacement value. +An *arg_id* selects which argument to format. It may be a non-negative +integer (positional reference) or an identifier matching the name of an +argument passed via [`fmt::arg`](api.md#arg) (named reference). When *arg_id* +is omitted, arguments are consumed in left-to-right order; this *automatic* +indexing must be used uniformly throughout the format string — mixing +automatic and explicit numeric ids is a compile-time error (or a +`format_error` at runtime). -See also the [Format Specification -Mini-Language](#format-specification-mini-language) section. +A *format_spec*, introduced by `:`, describes how the value should be +rendered. Its grammar is type-dependent; the form used by the standard +built-in types is documented in the next section. -If the numerical arg_ids in a format string are 0, 1, 2, ... in sequence, -they can all be omitted (not just some) and the numbers 0, 1, 2, ... will be -automatically inserted in that order. - -Named arguments can be referred to by their names or indices. - -Some simple format string examples: +For example: ```c++ -"First, thou shalt count to {0}" // References the first argument -"Bring me a {}" // Implicitly references the first argument -"From {} to {}" // Same as "From {0} to {1}" +fmt::format("hello, {}", "world"); +// Result: "hello, world" + +fmt::format("{1}, {0}!", "world", "hello"); +// Result: "hello, world!" + +fmt::format("{greeting}, {name}!", + fmt::arg("greeting", "hi"), fmt::arg("name", "fmt")); +// Result: "hi, fmt!" ``` -The *format_spec* field contains a specification of how the value should -be presented, including such details as field width, alignment, padding, -decimal precision and so on. Each value type can define its own -"formatting mini-language" or interpretation of the *format_spec*. +A *width* or *precision* inside a *format_spec* may itself be written as a +nested replacement field — `{}` or `{arg_id}` — in which case it takes its +value from an integer argument at runtime. Nested fields accept only an +*arg_id*; they cannot themselves contain a *format_spec*. -Most built-in types support a common formatting mini-language, which is -described in the next section. +## Format Specification -A *format_spec* field can also include nested replacement fields in -certain positions within it. These nested replacement fields can contain -only an argument id; format specifications are not allowed. This allows -the formatting of a value to be dynamically specified. - -See the [Format Examples](#format-examples) section for some examples. - -## Format Specification Mini-Language - -"Format specifications" are used within replacement fields contained within a -format string to define how individual values are presented. Each formattable -type may define how the format specification is to be interpreted. - -Most built-in types implement the following options for format -specifications, although some of the formatting options are only -supported by the numeric types. - -The general form of a *standard format specifier* is: +The grammar below describes the *format_spec* shared by the built-in +types — integers, floating-point values, characters, strings, booleans, and +pointers — as well as by any user-defined type whose `formatter` reuses +fmt's parser.

 
-The *fill* character can be any Unicode code point other than `'{'` or `'}'`. -The presence of a fill character is signaled by the character following it, -which must be one of the alignment options. If the second character of -*format_spec* is not a valid alignment option, then it is assumed that both -the fill character and the alignment option are absent. +Whether a particular option is meaningful depends on the value being +formatted; options that do not apply to a value's type are diagnosed at +compile time when possible and otherwise raise a `format_error`. -The meaning of the various alignment options is as follows: +### Fill and alignment - - - - - - - - - - - - - - - - - -
OptionMeaning
'<' - Forces the field to be left-aligned within the available space (this is the - default for most objects). -
'>' - Forces the field to be right-aligned within the available space (this is - the default for numbers). -
'^'Forces the field to be centered within the available space.
+The *align* field selects where padding is placed when *width* makes the +field wider than the value's natural rendering. -Note that unless a minimum field width is defined, the field width will -always be the same size as the data to fill it, so that the alignment -option has no meaning in this case. +| Option | Effect | +|--------|-------------------------------------------------------------------| +| `<` | Left-align; pad on the right. Default for non-numeric types. | +| `>` | Right-align; pad on the left. Default for numeric types. | +| `^` | Center the value; if the padding cannot be split evenly, the extra padding character goes on the right. | -The *sign* option is only valid for floating point and signed integer types, -and can be one of the following: +The *fill* character is any single Unicode code point other than `{` or `}`, +encoded the same way as the format string. It supplies the padding character +in place of the default space. A fill character is recognized only when it is +immediately followed by an *align* character — otherwise it would be +indistinguishable from an option in another position — so to use a custom +fill you must also specify an alignment. - - - - - - - - - - - - - - - - - -
OptionMeaning
'+' - Indicates that a sign should be used for both nonnegative as well as - negative numbers. -
'-' - Indicates that a sign should be used only for negative numbers (this is the - default behavior). -
space - Indicates that a leading space should be used on nonnegative numbers, and a - minus sign on negative numbers. -
+Alignment has no observable effect when the value's natural rendering is +already at least as wide as *width*; the value is never truncated to fit. -The `'#'` option causes the "alternate form" to be used for the -conversion. The alternate form is defined differently for different -types. This option is only valid for integer and floating-point types. -For integers, when binary, octal, or hexadecimal output is used, this -option adds the prefix respective `"0b"` (`"0B"`), `"0"`, or `"0x"` -(`"0X"`) to the output value. Whether the prefix is lower-case or -upper-case is determined by the case of the type specifier, for example, -the prefix `"0x"` is used for the type `'x'` and `"0X"` is used for -`'X'`. For floating-point numbers the alternate form causes the result -of the conversion to always contain a decimal-point character, even if -no digits follow it. Normally, a decimal-point character appears in the -result of these conversions only if a digit follows it. In addition, for -`'g'` and `'G'` conversions, trailing zeros are not removed from the -result. +```c++ +fmt::format("[{:<10}]", "42"); // Result: "[42 ]" +fmt::format("[{:>10}]", "42"); // Result: "[ 42]" +fmt::format("[{:^10}]", "42"); // Result: "[ 42 ]" +fmt::format("[{:*^10}]", "42"); // Result: "[****42****]" - '*' as fill +``` -*width* is a decimal integer defining the minimum field width. If not -specified, then the field width will be determined by the content. +### Sign -Preceding the *width* field by a zero (`'0'`) character enables -sign-aware zero-padding for numeric types. It forces the padding to be -placed after the sign or base (if any) but before the digits. This is -used for printing fields in the form "+000000120". This option is only -valid for numeric types and it has no effect on formatting of infinity -and NaN. This option is ignored when any alignment specifier is present. +The *sign* field controls how the sign of a numeric value is emitted. It +applies to signed integer and floating-point types only. -The *precision* is a decimal number indicating how many digits should be -displayed after the decimal point for a floating-point value formatted -with `'f'` and `'F'`, or before and after the decimal point for a -floating-point value formatted with `'g'` or `'G'`. For non-number types -the field indicates the maximum field size - in other words, how many -characters will be used from the field content. The *precision* is not -allowed for integer, character, Boolean, and pointer values. Note that a -C string must be null-terminated even if precision is specified. +| Option | Effect | +|--------|-------------------------------------------------------------------| +| `+` | Always emit a sign (`+` for nonnegative values, `-` for negative).| +| `-` | Emit `-` only for negative values. This is the default. | +| space | Emit a leading space for nonnegative values and `-` for negative ones; useful for aligning columns of signed numbers. | -The `'L'` option uses the current locale setting to insert the appropriate -number separator characters. This option is only valid for numeric types. +The sign of `-0.0` is preserved in floating-point output. -Finally, the *type* determines how the data should be presented. +```c++ +fmt::format("{:+d} {:+d}", 7, -7); // Result: "+7 -7" +fmt::format("{: d} {: d}", 7, -7); // Result: " 7 -7" +``` -The available string presentation types are: +### Alternate form (`#`) - - - - - - - - - - - - - - - - - -
TypeMeaning
's' - String format. This is the default type for strings and may be omitted. -
'?'Debug format. The string is quoted and special characters escaped.
noneThe same as 's'.
+The `#` flag selects an *alternate form* whose exact meaning depends on the +presentation type: -The available character presentation types are: +- For integers rendered in binary, octal, or hexadecimal, it prepends the + appropriate base prefix (`0b`/`0B`, `0`, or `0x`/`0X`). The case of the + prefix follows the case of the type specifier — `0x` for `x`, `0X` for + `X`, and so on. +- For floating-point values, it forces the decimal point to appear in the + output even if no fractional digits would otherwise be emitted, and + prevents the `g`/`G` presentation types from removing trailing zeros from + the significand. - - - - - - - - - - - - - - - - - -
TypeMeaning
'c' - Character format. This is the default type for characters and may be - omitted. -
'?'Debug format. The character is quoted and special characters escaped.
noneThe same as 'c'.
+The `#` flag is not accepted by non-numeric types. -The available integer presentation types are: +### Zero padding (`0`) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeMeaning
'b' - Binary format. Outputs the number in base 2. Using the '#' - option with this type adds the prefix "0b" to the output value. -
'B' - Binary format. Outputs the number in base 2. Using the '#' - option with this type adds the prefix "0B" to the output value. -
'c'Character format. Outputs the number as a character.
'd'Decimal integer. Outputs the number in base 10.
'o'Octal format. Outputs the number in base 8.
'x' - Hex format. Outputs the number in base 16, using lower-case letters for the - digits above 9. Using the '#' option with this type adds the - prefix "0x" to the output value. -
'X' - Hex format. Outputs the number in base 16, using upper-case letters for the - digits above 9. Using the '#' option with this type adds the - prefix "0X" to the output value. -
noneThe same as 'd'.
+A `0` placed immediately before *width* enables sign-aware zero padding for +numeric types. Zeros are inserted between the sign (or base prefix, if any) +and the most significant digit, so that a sign or `0x` prefix stays adjacent +to the digits rather than being separated by spaces. For example, `{:+08d}` +applied to `120` produces `+0000120`. -Integer presentation types can also be used with character and Boolean values -with the only exception that `'c'` cannot be used with `bool`. Boolean values -are formatted using textual representation, either `true` or `false`, if the -presentation type is not specified. +Zero padding: -The available presentation types for floating-point values are: +- applies only to numeric types; +- has no effect on `inf` or `nan`; +- is ignored when an explicit *align* is also present. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeMeaning
'a' - Hexadecimal floating point format. Prints the number in base 16 with - prefix "0x" and lower-case letters for digits above 9. - Uses 'p' to indicate the exponent. -
'A' - Same as 'a' except it uses upper-case letters for the - prefix, digits above 9 and to indicate the exponent. -
'e' - Exponent notation. Prints the number in scientific notation using - the letter 'e' to indicate the exponent. -
'E' - Exponent notation. Same as 'e' except it uses an - upper-case 'E' as the separator character. -
'f'Fixed point. Displays the number as a fixed-point number.
'F' - Fixed point. Same as 'f', but converts nan - to NAN and inf to INF. -
'g' -

General format. For a given precision p >= 1, - this rounds the number to p significant digits and then - formats the result in either fixed-point format or in scientific - notation, depending on its magnitude.

-

A precision of 0 is treated as equivalent to a precision - of 1.

-
'G' - General format. Same as 'g' except switches to - 'E' if the number gets too large. The representations of - infinity and NaN are uppercased, too. -
none - Similar to 'g', except that the default precision is as - high as needed to represent the particular value. -
+### Width -The available presentation types for pointers are: +*width* is a non-negative decimal integer giving the minimum number of +characters that the field should occupy. If the formatted value is shorter +than *width*, it is padded according to *align* and *fill*; if it is longer, +the value is written in full. *width* never causes the value to be +truncated. - - - - - - - - - - - - - -
TypeMeaning
'p' - Pointer format. This is the default type for pointers and may be omitted. -
noneThe same as 'p'.
+To supply *width* at runtime, write the field as `{}` to consume the next +argument, or as `{arg_id}` to reference an integer argument by position or +by name. -## Chrono Format Specifications +When formatting strings, "width" is measured in display columns using a +Unicode-aware estimate (East Asian wide and fullwidth characters, plus +common emoji ranges, count as two columns; everything else counts as one). +This keeps fixed *width* values visually consistent in monospace renderings +that combine Latin and CJK text. -Format specifications for chrono duration and time point types as well as -`std::tm` have the following syntax: +```c++ +fmt::format("[{:6}]", 42); +// Result: "[ 42]" - right-aligned by default +fmt::format("[{:6}]", "hi"); +// Result: "[hi ]" - left-aligned by default +fmt::format("[{:{}}]", 42, 6); +// Result: "[ 42]" - width from an argument +``` + +### Precision + +*precision* is a non-negative decimal integer (introduced by `.`) whose +meaning depends on the value being formatted. As with *width*, it may be +supplied as a nested replacement field for runtime evaluation. + +| Type | Meaning of `.precision` | +|-------------------------------|-----------------------------------------------| +| `e`, `E`, `f`, `F` | Digits emitted after the decimal point. | +| `g`, `G` | Total number of significant digits. | +| `a`, `A` | Digits after the decimal point in the hexadecimal significand. If omitted, just enough digits are emitted to round-trip the value exactly. | +| Strings (`s`, `?`, or default) | Upper bound on the number of code points copied from the value. | + +A *precision* is not accepted for integer, character, boolean, or pointer +types. When a *precision* limits the number of characters taken from a C +string, the string must still be null-terminated. + +```c++ +fmt::format("{:.2f}", 3.14159); // Result: "3.14" +fmt::format("{:.3g}", 3.14159); // Result: "3.14" +fmt::format("{:.4}", "hello, world"); // Result: "hell" +fmt::format("{:.{}f}", 3.14159, 4); +// Result: "3.1416" - precision from an argument +``` + +### Locale (`L`) + +The `L` flag selects locale-sensitive formatting for numeric types. The +formatter inspects the C++ locale supplied to the formatting function (or +the global locale, if none was passed) and inserts the locale's digit +grouping characters and — for floating-point values — its decimal point. +The flag has no effect on non-numeric types. + +```c++ +auto loc = std::locale("en_US.UTF-8"); +fmt::format(loc, "{:L}", 1234567890); // Result: "1,234,567,890" +fmt::format(loc, "{:.2Lf}", 1234567.89); // Result: "1,234,567.89" +``` + +### Presentation type + +The *type* field chooses the representation for the value. Specifiers are +grouped below by the value categories they apply to. + +**Integers, booleans, and characters:** + +| Type | Effect | +|------|---------------------------------------------------------------------| +| `b` | Base 2. The `#` flag adds a `0b` prefix. | +| `B` | Base 2. The `#` flag adds a `0B` prefix. | +| `c` | Render the integer as the character with that code point. Not allowed for `bool`. | +| `d` | Base 10. The default for integer types. | +| `o` | Base 8. | +| `x` | Base 16, lower-case digits. The `#` flag adds a `0x` prefix. | +| `X` | Base 16, upper-case digits. The `#` flag adds a `0X` prefix. | +| none | Same as `d` for integers, `c` for characters, and the textual form (`true`/`false`) for `bool`. | + +```c++ +fmt::format("{:d} {:#x} {:#o} {:#b}", 42, 42, 42, 42); +// Result: "42 0x2a 052 0b101010" + +fmt::format("{:#06x}", 0xfe); // # adds the prefix, 06 zero-pads to width 6 +// Result: "0x00fe" +``` + +**Floating-point values:** + +| Type | Effect | +|------|---------------------------------------------------------------------| +| `a` | Hexadecimal-significand form (e.g. `0x1.8p+1`). Lower-case digits and a lower-case `p` for the binary exponent. The `0x` prefix is always emitted, matching printf's `%a`. The `#` flag forces a decimal point (e.g. `0x1.p+1`). +| `A` | Same as `a`, but upper-case throughout. | +| `e` | Scientific notation with a lower-case `e` for the decimal exponent. | +| `E` | Scientific notation with an upper-case `E`. | +| `f` | Fixed-point notation. | +| `F` | Same as `f`, but renders `nan` as `NAN` and `inf` as `INF`. | +| `g` | General form: scientific notation when the exponent would be less than −4 or not less than the precision, otherwise fixed-point; trailing zeros are removed from the fractional part unless `#` is set. A precision of `0` is interpreted as `1`. | +| `G` | Same as `g`, but uses `E` for the exponent and upper-case `INF`/`NAN`. | +| none | Shortest round-trip representation: the formatted value, when parsed back into the same floating-point type, reproduces the input bit for bit. | + +**Strings and characters:** + +| Type | Effect | +|------|---------------------------------------------------------------------| +| `s` | Plain string output. Default for string types and for `bool` (which is rendered as `true` or `false`). | +| `c` | Character output. Default for character types. Not allowed for `bool`. | +| `?` | Debug output: the value is wrapped in single quotes (characters) or double quotes (strings), and non-printable, non-ASCII, and special characters are escaped using C-style escape sequences such as `\n`, `\t`, `\"`, and `\u{...}`. | +| none | Same as `s` for strings and `bool`, and as `c` for characters. | + +```c++ +fmt::format("{}", "tab\there"); // Result contains a literal tab character. +fmt::format("{:?}", "tab\there"); // Result: "\"tab\\there\"" +``` + +**Pointers:** + +| Type | Effect | +|------|---------------------------------------------------------------------| +| `p` | Hexadecimal address prefixed by `0x`. Default for pointer types. | +| none | Same as `p`. | + +A C string (`char*` or `const char*`) accepts both the string presentation +types and `p`, so the same value can be formatted as either text or an +address. + +## Chrono Format Specification + +The format specification for chrono duration and time point types as well +as `std::tm` has the following syntax:

+
+auto t = std::tm();
+t.tm_year = 2010 - 1900;
+t.tm_mon = 7;
+t.tm_mday = 4;
+t.tm_hour = 12;
+t.tm_min = 15;
+t.tm_sec = 58;
+fmt::print("{:%Y-%m-%d %H:%M:%S}", t);
+// Prints: 2010-08-04 12:15:58
+```
+
+## Range Format Specification
+
+The format specification for range types has the following syntax:
 
 
range_format_spec ::= ["n"][range_type][":" range_underlying_spec]
@@ -765,87 +673,11 @@ fmt::print("{:n:f}", std::array{std::numbers::pi, std::numbers::e});
 // Output: 3.141593, 2.718282
 ```
 
-## Format Examples
+## A Combined Example
 
-This section contains examples of the format syntax and comparison with
-the `printf` formatting.
-
-In most of the cases the syntax is similar to the `printf` formatting,
-with the addition of the `{}` and with `:` used instead of `%`. For
-example, `"%03.2f"` can be translated to `"{:03.2f}"`.
-
-The new format syntax also supports new and different options, shown in
-the following examples.
-
-Accessing arguments by position:
-
-```c++
-fmt::format("{0}, {1}, {2}", 'a', 'b', 'c');
-// Result: "a, b, c"
-fmt::format("{}, {}, {}", 'a', 'b', 'c');
-// Result: "a, b, c"
-fmt::format("{2}, {1}, {0}", 'a', 'b', 'c');
-// Result: "c, b, a"
-fmt::format("{0}{1}{0}", "abra", "cad");  // arguments' indices can be repeated
-// Result: "abracadabra"
-```
-
-Aligning the text and specifying a width:
-
-```c++
-fmt::format("{:<30}", "left aligned");
-// Result: "left aligned                  "
-fmt::format("{:>30}", "right aligned");
-// Result: "                 right aligned"
-fmt::format("{:^30}", "centered");
-// Result: "           centered           "
-fmt::format("{:*^30}", "centered");  // use '*' as a fill char
-// Result: "***********centered***********"
-```
-
-Dynamic width:
-
-```c++
-fmt::format("{:<{}}", "left aligned", 30);
-// Result: "left aligned                  "
-```
-
-Dynamic precision:
-
-```c++
-fmt::format("{:.{}f}", 3.14, 1);
-// Result: "3.1"
-```
-
-Replacing `%+f`, `%-f`, and `% f` and specifying a sign:
-
-```c++
-fmt::format("{:+f}; {:+f}", 3.14, -3.14);  // show it always
-// Result: "+3.140000; -3.140000"
-fmt::format("{: f}; {: f}", 3.14, -3.14);  // show a space for positive numbers
-// Result: " 3.140000; -3.140000"
-fmt::format("{:-f}; {:-f}", 3.14, -3.14);  // show only the minus -- same as '{:f}; {:f}'
-// Result: "3.140000; -3.140000"
-```
-
-Replacing `%x` and `%o` and converting the value to different bases:
-
-```c++
-fmt::format("int: {0:d};  hex: {0:x};  oct: {0:o}; bin: {0:b}", 42);
-// Result: "int: 42;  hex: 2a;  oct: 52; bin: 101010"
-// with 0x or 0 or 0b as prefix:
-fmt::format("int: {0:d};  hex: {0:#x};  oct: {0:#o};  bin: {0:#b}", 42);
-// Result: "int: 42;  hex: 0x2a;  oct: 052;  bin: 0b101010"
-```
-
-Padded hex byte with prefix and always prints both hex characters:
-
-```c++
-fmt::format("{:#04x}", 0);
-// Result: "0x00"
-```
-
-Box drawing using Unicode fill:
+The example below ties together several elements introduced above — nested
+replacement fields, fill characters, and centering — to draw a fixed-width
+box around a message:
 
 ```c++
 fmt::print(
@@ -854,35 +686,10 @@ fmt::print(
     "└{0:─^{2}}┘\n", "", "Hello, world!", 20);
 ```
 
-prints:
+Output:
 
 ```
 ┌────────────────────┐
 │   Hello, world!    │
 └────────────────────┘
 ```
-
-Using type-specific formatting:
-
-```c++
-#include 
-
-auto t = tm();
-t.tm_year = 2010 - 1900;
-t.tm_mon = 7;
-t.tm_mday = 4;
-t.tm_hour = 12;
-t.tm_min = 15;
-t.tm_sec = 58;
-fmt::print("{:%Y-%m-%d %H:%M:%S}", t);
-// Prints: 2010-08-04 12:15:58
-```
-
-Using the comma as a thousands separator:
-
-```c++
-#include 
-
-auto s = fmt::format(std::locale("en_US.UTF-8"), "{:L}", 1234567890);
-// s == "1,234,567,890"
-```
diff --git a/include/fmt/args.h b/include/fmt/args.h
index 7b04ef0d..3283e6fa 100644
--- a/include/fmt/args.h
+++ b/include/fmt/args.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - dynamic argument lists
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/include/fmt/base.h b/include/fmt/base.h
index 48adb794..8f9955bf 100644
--- a/include/fmt/base.h
+++ b/include/fmt/base.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - the base API for char/UTF-8
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -21,7 +21,7 @@
 #endif
 
 // The fmt library version in the form major * 10000 + minor * 100 + patch.
-#define FMT_VERSION 120101
+#define FMT_VERSION 120200
 
 // Detect compiler versions.
 #if defined(__clang__) && !defined(__ibmxl__)
@@ -223,13 +223,26 @@
 #else
 #  define FMT_PRAGMA_CLANG(x)
 #endif
+#if FMT_MSC_VERSION
+#  define FMT_PRAGMA_MSVC(x) __pragma(x)
+#else
+#  define FMT_PRAGMA_MSVC(x)
+#endif
+
+#ifndef FMT_USE_OPTIMIZE_PRAGMA
+#  define FMT_USE_OPTIMIZE_PRAGMA 1
+#endif
 
 // Enable minimal optimizations for more compact code in debug mode.
 FMT_PRAGMA_GCC(push_options)
-#if !defined(__OPTIMIZE__) && !defined(__CUDACC__) && !defined(FMT_MODULE)
+#if FMT_USE_OPTIMIZE_PRAGMA && !defined(__OPTIMIZE__) && \
+    !defined(__CUDACC__) && !defined(FMT_MODULE)
 FMT_PRAGMA_GCC(optimize("Og"))
 #endif
 
+FMT_PRAGMA_MSVC(warning(push))
+FMT_PRAGMA_MSVC(warning(disable : 4702))
+
 #ifdef FMT_DEPRECATED
 // Use the provided definition.
 #elif FMT_HAS_CPP14_ATTRIBUTE(deprecated)
@@ -484,6 +497,15 @@ inline FMT_CONSTEXPR auto get_container(OutputIt it) ->
   };
   return *accessor(it).container;
 }
+
+template 
+struct is_contiguous : std::false_type {};
+template 
+struct is_contiguous().data()),
+                            decltype(std::declval().size()),
+                            decltype(std::declval().operator[](size_t()))>>
+    : std::true_type {};
 }  // namespace detail
 
 // Parsing-related public API and forward declarations.
@@ -587,9 +609,6 @@ using string_view = basic_string_view;
 template  class basic_appender;
 using appender = basic_appender;
 
-// Checks whether T is a container with contiguous storage.
-template  struct is_contiguous : std::false_type {};
-
 class context;
 template  class generic_context;
 template  class parse_context;
@@ -608,6 +627,8 @@ using buffered_context =
     conditional_t::value, context,
                   generic_context, Char>>;
 
+template  struct is_contiguous : detail::is_contiguous {};
+
 template  class basic_format_arg;
 template  class basic_format_args;
 
@@ -1858,6 +1879,54 @@ class fixed_buffer_traits {
   }
 };
 
+template 
+struct has_append : std::false_type {};
+
+template 
+struct has_append())
+                                      .append(std::declval(),
+                                              std::declval()))>>
+    : std::true_type {};
+
+template 
+struct has_insert : std::false_type {};
+
+template 
+struct has_insert<
+    OutputIt, T,
+    void_t())
+                        .insert({}, std::declval(), std::declval()))>>
+    : std::true_type {};
+
+// An optimized version of std::copy with the output value type (T).
+template () &&
+                        has_append())>
+FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
+  get_container(out).append(begin, end);
+  return out;
+}
+
+template () &&
+                        !has_append() &&
+                        has_insert())>
+FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
+  auto& c = get_container(out);
+  c.insert(c.end(), begin, end);
+  return out;
+}
+
+template () ||
+                        !(has_append() ||
+                          has_insert()))>
+FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
+  while (begin != end) *out++ = static_cast(*begin++);
+  return out;
+}
+
 // A buffer that writes to an output iterator when flushed.
 template 
 class iterator_buffer : public Traits, public buffer {
@@ -1875,7 +1944,7 @@ class iterator_buffer : public Traits, public buffer {
     this->clear();
     const T* begin = data_;
     const T* end = begin + this->limit(size);
-    while (begin != end) *out_++ = *begin++;
+    out_ = copy(begin, end, out_);
   }
 
  public:
@@ -2533,7 +2602,7 @@ template  class basic_format_args {
   FMT_CONSTEXPR auto get(int id) const -> format_arg {
     auto arg = format_arg();
     if (!is_packed()) {
-      if (id < max_size()) arg = args_[id];
+      if (unsigned(id) < unsigned(max_size())) arg = args_[id];
       return arg;
     }
     if (unsigned(id) >= detail::max_packed_args) return arg;
@@ -2644,16 +2713,16 @@ template  struct fstring {
     static_assert(count<(is_view>::value &&
                          std::is_reference::value)...>() == 0,
                   "passing views as lvalues is disallowed");
-    if (FMT_USE_CONSTEVAL) parse_format_string(s, checker(s, arg_pack()));
+    if (FMT_USE_CONSTEVAL)
+      parse_format_string(str, checker(str, arg_pack()));
     constexpr bool unused = detail::enforce_compile_checks();
     (void)unused;
   }
   template ::value)>
   FMT_CONSTEVAL FMT_ALWAYS_INLINE fstring(const S& s) : str(s) {
-    auto sv = string_view(str);
     if (FMT_USE_CONSTEVAL)
-      detail::parse_format_string(sv, checker(sv, arg_pack()));
+      detail::parse_format_string(str, checker(str, arg_pack()));
     constexpr bool unused = detail::enforce_compile_checks();
     (void)unused;
   }
@@ -2877,6 +2946,7 @@ FMT_INLINE void println(format_string fmt, T&&... args) {
 }
 
 FMT_PRAGMA_GCC(pop_options)
+FMT_PRAGMA_MSVC(warning(pop))
 FMT_END_EXPORT
 FMT_END_NAMESPACE
 
diff --git a/include/fmt/chrono.h b/include/fmt/chrono.h
index 752b9699..46590074 100644
--- a/include/fmt/chrono.h
+++ b/include/fmt/chrono.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - chrono support
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -934,10 +934,9 @@ inline auto to_nonnegative_int(T value, Int upper) -> Int {
 }
 template ::value)>
 inline auto to_nonnegative_int(T value, Int upper) -> Int {
-  auto int_value = static_cast(value);
-  if (int_value < 0 || value > static_cast(upper))
+  if (value < 0 || value >= static_cast(upper) + 1)
     FMT_THROW(format_error("invalid value"));
-  return int_value;
+  return static_cast(value);
 }
 
 constexpr auto pow10(std::uint32_t n) -> long long {
@@ -1192,6 +1191,7 @@ class tm_writer {
 
   template ::value)>
   void format_tz_name(const T& tm) {
+    if (!tm.tm_zone) FMT_THROW(format_error("no timezone"));
     out_ = write_tm_str(out_, tm.tm_zone, loc_);
   }
   template ::value)>
diff --git a/include/fmt/color.h b/include/fmt/color.h
index 153784f5..bf0c32d6 100644
--- a/include/fmt/color.h
+++ b/include/fmt/color.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - color support
 //
-// Copyright (c) 2018 - present, Victor Zverovich and fmt contributors
+// Copyright (c) 2018 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -471,7 +471,7 @@ template  inline void reset_color(buffer& buffer) {
 template  struct styled_arg : view {
   const T& value;
   text_style style;
-  styled_arg(const T& v, text_style s) : value(v), style(s) {}
+  FMT_CONSTEXPR styled_arg(const T& v, text_style s) : value(v), style(s) {}
 };
 
 template 
@@ -528,6 +528,42 @@ void print(text_style ts, format_string fmt, T&&... args) {
   return print(stdout, ts, fmt, std::forward(args)...);
 }
 
+inline void vprintln(FILE* f, text_style ts, string_view fmt,
+                     format_args args) {
+  auto buf = memory_buffer();
+  detail::vformat_to(buf, ts, fmt, args);
+  buf.push_back('\n');
+  print(f, FMT_STRING("{}"), string_view(buf.begin(), buf.size()));
+}
+
+/**
+ * Formats a string and prints it to the specified file stream followed by a
+ * newline, using ANSI escape sequences to specify text formatting.
+ *
+ * **Example**:
+ *
+ *     fmt::println(fmt::emphasis::bold | fg(fmt::color::red),
+ *                  "Elapsed time: {0:.2f} seconds", 1.23);
+ */
+template 
+void println(FILE* f, text_style ts, format_string fmt, T&&... args) {
+  vprintln(f, ts, fmt.str, vargs{{args...}});
+}
+
+/**
+ * Formats a string and prints it to stdout followed by a newline, using ANSI
+ * escape sequences to specify text formatting.
+ *
+ * **Example**:
+ *
+ *     fmt::println(fmt::emphasis::bold | fg(fmt::color::red),
+ *                  "Elapsed time: {0:.2f} seconds", 1.23);
+ */
+template 
+void println(text_style ts, format_string fmt, T&&... args) {
+  return println(stdout, ts, fmt, std::forward(args)...);
+}
+
 inline auto vformat(text_style ts, string_view fmt, format_args args)
     -> std::string {
   auto buf = memory_buffer();
@@ -583,8 +619,8 @@ inline auto format_to(OutputIt out, text_style ts, format_string fmt,
 template 
 struct formatter, Char> : formatter {
   template 
-  auto format(const detail::styled_arg& arg, FormatContext& ctx) const
-      -> decltype(ctx.out()) {
+  FMT_CONSTEXPR auto format(const detail::styled_arg& arg,
+                            FormatContext& ctx) const -> decltype(ctx.out()) {
     const auto& ts = arg.style;
     auto out = ctx.out();
 
diff --git a/include/fmt/compile.h b/include/fmt/compile.h
index 5c6820f7..2a0be049 100644
--- a/include/fmt/compile.h
+++ b/include/fmt/compile.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - experimental format string compilation
 //
-// Copyright (c) 2012 - present, Victor Zverovich and fmt contributors
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -178,6 +178,10 @@ template  struct field {
     if constexpr (std::is_convertible>::value) {
       auto s = basic_string_view(arg);
       return copy(s.begin(), s.end(), out);
+    } else if constexpr (use_format_as::value) {
+      return write(out, format_as(arg));
+    } else if constexpr (use_format_as_member::value) {
+      return write(out, formatter::format_as(arg));
     } else {
       return write(out, arg);
     }
diff --git a/include/fmt/core.h b/include/fmt/core.h
index f1490a9f..eff2ae89 100644
--- a/include/fmt/core.h
+++ b/include/fmt/core.h
@@ -1,3 +1,10 @@
+// Formatting library for C++ - core API
+//
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
 #include "base.h"
 
 // Using fmt::format via fmt/core.h has been deprecated since version 11
diff --git a/include/fmt/fmt-c.h b/include/fmt/fmt-c.h
index 1ce41d37..3f8714d4 100644
--- a/include/fmt/fmt-c.h
+++ b/include/fmt/fmt-c.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - the C API
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -10,6 +10,24 @@
 
 #include   // bool
 #include    // size_t
+#include     // FILE
+
+#if !defined(FMT_HEADER_ONLY) && defined(_WIN32)
+#  if defined(FMT_LIB_EXPORT)
+#    define FMT_CAPI __declspec(dllexport)
+#  elif defined(FMT_SHARED)
+#    define FMT_CAPI __declspec(dllimport)
+#  endif
+#elif defined(FMT_LIB_EXPORT) || defined(FMT_SHARED)
+#  ifdef __GNUC__
+#    define FMT_CAPI __attribute__((visibility("default")))
+#  else
+#    define FMT_CAPI
+#  endif
+#endif
+#ifndef FMT_CAPI
+#  define FMT_CAPI
+#endif
 
 #ifdef __cplusplus
 extern "C" {
@@ -44,8 +62,10 @@ typedef struct {
 
 enum { fmt_error = -1, fmt_error_invalid_arg = -2 };
 
-int fmt_vformat(char* buffer, size_t size, const char* fmt, const fmt_arg* args,
-                size_t num_args);
+int FMT_CAPI fmt_vformat(char* buffer, size_t size, const char* fmt,
+                         const fmt_arg* args, size_t num_args);
+int FMT_CAPI fmt_vprint(FILE* stream, const char* fmt, const fmt_arg* args,
+                        size_t num_args);
 
 #ifdef __cplusplus
 }
@@ -89,7 +109,7 @@ static inline fmt_arg fmt_from_ptr(const void* x) {
   return (fmt_arg){.type = fmt_pointer, .value.pointer = x};
 }
 
-void fmt_unsupported_type(void);
+void FMT_CAPI fmt_unsupported_type(void);
 
 #  if !defined(_MSC_VER) || defined(__clang__)
 typedef signed char fmt_signed_char;
@@ -181,12 +201,17 @@ typedef enum {} fmt_signed_char;
     (fmt_arg[]) { FMT_MAP(FMT_MAKE_ARG, ##__VA_ARGS__) }
 #  define FMT_EXPAND(v) v
 
-#  define fmt_format(buffer, size, fmt, ...)                               \
-    fmt_vformat(                                                           \
-        (buffer), (size), (fmt),                                           \
-        FMT_EXPAND(FMT_VA_SELECT(                                          \
-            FMT_MAKE_NULL, FMT_MAKE_ARGLIST, ##__VA_ARGS__)(__VA_ARGS__)), \
-        FMT_NARG(, ##__VA_ARGS__))
+#  define FMT_FORMAT_ARGS(fmt, ...)                               \
+    (fmt),                                                        \
+        FMT_EXPAND(FMT_VA_SELECT(FMT_MAKE_NULL, FMT_MAKE_ARGLIST, \
+                                 ##__VA_ARGS__)(__VA_ARGS__)),    \
+        FMT_NARG(, ##__VA_ARGS__)
+
+#  define fmt_format(buffer, size, fmt, ...) \
+    fmt_vformat((buffer), (size), FMT_FORMAT_ARGS((fmt), ##__VA_ARGS__))
+
+#  define fmt_print(stream, fmt, ...) \
+    fmt_vprint((stream), FMT_FORMAT_ARGS((fmt), ##__VA_ARGS__))
 
 #endif  // __cplusplus
 
diff --git a/include/fmt/format-inl.h b/include/fmt/format-inl.h
index cc110198..ecd3ffec 100644
--- a/include/fmt/format-inl.h
+++ b/include/fmt/format-inl.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - implementation
 //
-// Copyright (c) 2012 - 2016, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -8,15 +8,17 @@
 #ifndef FMT_FORMAT_INL_H_
 #define FMT_FORMAT_INL_H_
 
+#ifdef __SANITIZE_THREAD__
+extern "C" void __tsan_acquire(void*);
+extern "C" void __tsan_release(void*);
+#endif
+
 #ifndef FMT_MODULE
 #  include   // ptrdiff_t
 
 #  include 
 #  include   // errno
-#  include 
-#  include 
-#  include 
-#  include   // std::bad_alloc
+#  include      // std::bad_alloc
 #endif
 
 #if defined(_WIN32) && !defined(FMT_USE_WRITE_CONSOLE)
@@ -1477,10 +1479,10 @@ template  struct span {
 };
 
 template  auto flockfile(F* f) -> decltype(_lock_file(f)) {
-  _lock_file(f);
+  return _lock_file(f);
 }
 template  auto funlockfile(F* f) -> decltype(_unlock_file(f)) {
-  _unlock_file(f);
+  return _unlock_file(f);
 }
 
 #ifndef getc_unlocked
@@ -1524,7 +1526,10 @@ template  class file_base {
       FMT_THROW(system_error(errno, FMT_STRING("ungetc failed")));
   }
 
-  void flush() { fflush(this->file_); }
+  void flush() {
+    if (fflush(this->file_) != 0)
+      FMT_THROW(system_error(errno, FMT_STRING("fflush failed")));
+  }
 };
 
 // A FILE wrapper for glibc.
@@ -1570,7 +1575,10 @@ template  class glibc_file : public file_base {
     return memchr(end, '\n', static_cast(size));
   }
 
-  void flush() { fflush_unlocked(this->file_); }
+  void flush() {
+    if (fflush_unlocked(this->file_) != 0)
+      FMT_THROW(system_error(errno, FMT_STRING("fflush failed")));
+  }
 };
 
 // A FILE wrapper for Apple's libc.
@@ -1696,6 +1704,9 @@ class file_print_buffer::value>>
  public:
   explicit file_print_buffer(F* f) : buffer(grow, size_t()), file_(f) {
     flockfile(f);
+#ifdef __SANITIZE_THREAD__
+    __tsan_acquire(f);
+#endif
     file_.init_buffer();
     auto buf = file_.get_write_buffer();
     set(buf.data, buf.size);
@@ -1703,7 +1714,10 @@ class file_print_buffer::value>>
   ~file_print_buffer() {
     file_.advance_write_buffer(size());
     bool flush = file_.needs_flush();
-    F* f = file_;    // Make funlockfile depend on the template parameter F
+    F* f = file_;  // Make funlockfile depend on the template parameter F.
+#ifdef __SANITIZE_THREAD__
+    __tsan_release(f);
+#endif
     funlockfile(f);  // for the system API detection to work.
     if (flush) fflush(file_);
   }
diff --git a/include/fmt/format.h b/include/fmt/format.h
index c9b95dbd..dcf51949 100644
--- a/include/fmt/format.h
+++ b/include/fmt/format.h
@@ -1,7 +1,7 @@
 /*
   Formatting library for C++
 
-  Copyright (c) 2012 - present, Victor Zverovich
+  Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 
   Permission is hereby granted, free of charge, to any person obtaining
   a copy of this software and associated documentation files (the
@@ -326,6 +326,9 @@ class uint128 {
       -> uint128 {
     return {lhs.hi_ & rhs.hi_, lhs.lo_ & rhs.lo_};
   }
+  friend constexpr auto operator~(const uint128& n) -> uint128 {
+    return {~n.hi_, ~n.lo_};
+  }
   friend FMT_CONSTEXPR auto operator+(const uint128& lhs, const uint128& rhs)
       -> uint128 {
     auto result = uint128(lhs);
@@ -548,60 +551,6 @@ FMT_CONSTEXPR20 auto fill_n(T* out, Size count, char value) -> T* {
   return out + count;
 }
 
-template 
-struct has_back_insert_iterator_container_append : std::false_type {};
-
-template 
-struct has_back_insert_iterator_container_append<
-    OutputIt, InputIt,
-    void_t())
-                        .append(std::declval(),
-                                std::declval()))>> : std::true_type {};
-
-template 
-struct has_back_insert_iterator_container_insert_at_end : std::false_type {};
-
-template 
-struct has_back_insert_iterator_container_insert_at_end<
-    OutputIt, InputIt,
-    void_t())
-                        .insert(get_container(std::declval()).end(),
-                                std::declval(),
-                                std::declval()))>> : std::true_type {};
-
-// An optimized version of std::copy with the output value type (T).
-template ::value&&
-                            has_back_insert_iterator_container_append<
-                                OutputIt, InputIt>::value)>
-FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
-  get_container(out).append(begin, end);
-  return out;
-}
-
-template ::value &&
-                        !has_back_insert_iterator_container_append<
-                            OutputIt, InputIt>::value &&
-                        has_back_insert_iterator_container_insert_at_end<
-                            OutputIt, InputIt>::value)>
-FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
-  auto& c = get_container(out);
-  c.insert(c.end(), begin, end);
-  return out;
-}
-
-template ::value &&
-                          (has_back_insert_iterator_container_append<
-                               OutputIt, InputIt>::value ||
-                           has_back_insert_iterator_container_insert_at_end<
-                               OutputIt, InputIt>::value)))>
-FMT_CONSTEXPR auto copy(InputIt begin, InputIt end, OutputIt out) -> OutputIt {
-  while (begin != end) *out++ = static_cast(*begin++);
-  return out;
-}
-
 template 
 FMT_CONSTEXPR auto copy(basic_string_view s, OutputIt out) -> OutputIt {
   return copy(s.begin(), s.end(), out);
@@ -1115,12 +1064,6 @@ template  FMT_CONSTEXPR auto count_digits_fallback(T n) -> int {
     count += 4;
   }
 }
-#if FMT_USE_INT128
-FMT_CONSTEXPR inline auto count_digits(native_uint128 n) -> int {
-  return count_digits_fallback(n);
-}
-#endif
-
 #ifdef FMT_BUILTIN_CLZLL
 // It is a separate function rather than a part of count_digits to workaround
 // the lack of static constexpr in constexpr functions.
@@ -1151,6 +1094,20 @@ FMT_CONSTEXPR20 inline auto count_digits(uint64_t n) -> int {
   return count_digits_fallback(n);
 }
 
+#if FMT_USE_INT128
+FMT_CONSTEXPR20 inline auto count_digits(native_uint128 n) -> int {
+  if (is_constant_evaluated()) return count_digits_fallback(n);
+  // 128-bit division is a slow library call, so reduce to 64-bit chunks via
+  // 10^19 (the largest power of 10 fitting in uint64_t).
+  const uint64_t pow10_19 = 10000000000000000000ULL;
+  if ((n >> 64) == 0) return count_digits(static_cast(n));
+  n /= pow10_19;
+  if ((n >> 64) == 0) return count_digits(static_cast(n)) + 19;
+  n /= pow10_19;
+  return count_digits(static_cast(n)) + 38;
+}
+#endif
+
 // Counts the number of digits in n. BITS = log2(radix).
 template 
 FMT_CONSTEXPR auto count_digits(UInt n) -> int {
@@ -1282,10 +1239,30 @@ template 
 FMT_CONSTEXPR20 auto do_format_decimal(Char* out, UInt value, int size)
     -> Char* {
   FMT_ASSERT(size >= count_digits(value), "invalid digit count");
+#if FMT_USE_INT128
+  // Avoid slow 128-bit division by formatting 64-bit chunks of 19 digits each.
+  if (!is_constant_evaluated() && num_bits() == 128) {
+    const uint64_t pow10_19 = 10000000000000000000ULL;
+    auto v = static_cast(value);
+    unsigned pos = to_unsigned(size);
+    while ((v >> 64) != 0) {
+      auto chunk = static_cast(v % pow10_19);
+      v /= pow10_19;
+      pos -= 19;
+      fill_n(out + pos, 19, Char('0'));
+      do_format_decimal(out + pos, chunk, 19);
+    }
+    auto top = static_cast(v);
+    int top_digits = count_digits(top);
+    pos -= to_unsigned(top_digits);
+    do_format_decimal(out + pos, top, top_digits);
+    return out + pos;
+  }
+#endif  // FMT_USE_INT128
   unsigned n = to_unsigned(size);
   while (value >= 100) {
     n -= 2;
-    if (!is_constant_evaluated() && sizeof(UInt) == 4) {
+    if (!is_constant_evaluated() && num_bits() == 32) {
       auto p = value * static_cast((1ull << 39) / 100 + 1);
       write2digits_i(out + n, p >> (39 - 7) & ((1 << 7) - 1));
       value = static_cast(p >> 39) +
@@ -2012,6 +1989,15 @@ FMT_CONSTEXPR inline void prefix_append(unsigned& prefix, unsigned value) {
   prefix += (1u + (value > 0xff ? 1 : 0)) << 24;
 }
 
+// Writes an integer as a character, treating chars as unsigned.
+template 
+FMT_CONSTEXPR auto write_int_chr(OutputIt out, UInt abs_value, bool negative,
+                                 const format_specs& specs) -> OutputIt {
+  if (negative || abs_value > max_value>())
+    report_error("character value out of range");
+  return write_char(out, static_cast(abs_value), specs);
+}
+
 // Writes a decimal integer with digit grouping.
 template 
 auto write_int(OutputIt out, UInt value, unsigned prefix,
@@ -2048,7 +2034,7 @@ auto write_int(OutputIt out, UInt value, unsigned prefix,
     format_base2e(1, appender(buffer), value, num_digits);
     break;
   case presentation_type::chr:
-    return write_char(out, static_cast(value), specs);
+    return write_int_chr(out, value, (prefix & 0xff) == '-', specs);
   }
 
   unsigned size = (prefix != 0 ? prefix >> 24 : 0) + to_unsigned(num_digits) +
@@ -2175,7 +2161,7 @@ FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg arg,
       prefix_append(prefix, unsigned(specs.upper() ? 'B' : 'b') << 8 | '0');
     break;
   case presentation_type::chr:
-    return write_char(out, static_cast(abs_value), specs);
+    return write_int_chr(out, abs_value, (prefix & 0xff) == '-', specs);
   }
 
   // Write an integer in the format
@@ -2208,29 +2194,19 @@ FMT_CONSTEXPR FMT_NOINLINE auto write_int_noinline(OutputIt out,
   return write_int(out, arg, specs);
 }
 
-template ::value &&
-                        !std::is_same::value &&
-                        !std::is_same::value)>
-FMT_CONSTEXPR FMT_INLINE auto write(basic_appender out, T value,
-                                    const format_specs& specs, locale_ref loc)
-    -> basic_appender {
-  if (specs.localized() && write_loc(out, value, specs, loc)) return out;
-  return write_int_noinline(out, make_write_int_arg(value, specs.sign()),
-                                  specs);
-}
-
-// An inlined version of write used in format string compilation.
 template ::value &&
                         !std::is_same::value &&
-                        !std::is_same::value &&
-                        !std::is_same>::value)>
+                        !std::is_same::value)>
 FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value,
                                     const format_specs& specs, locale_ref loc)
     -> OutputIt {
   if (specs.localized() && write_loc(out, value, specs, loc)) return out;
-  return write_int(out, make_write_int_arg(value, specs.sign()), specs);
+  auto arg = make_write_int_arg(value, specs.sign());
+  // Out of line for appenders to avoid bloat; inlined for compiled formatting.
+  if FMT_CONSTEXPR20 (std::is_same>::value)
+    return write_int_noinline(out, arg, specs);
+  return write_int(out, arg, specs);
 }
 
 template 
@@ -3215,8 +3191,9 @@ constexpr auto fractional_part_rounding_thresholds(int index) -> uint32_t {
   // It is equal to ceil(2^31 + 2^32/10^(k + 1)).
   // These are stored in a string literal because we cannot have static arrays
   // in constexpr functions and non-static ones are poorly optimized.
-  return U"\x9999999a\x828f5c29\x80418938\x80068db9\x8000a7c6\x800010c7"
-         U"\x800001ae\x8000002b"[index];
+  return uint32_t(u"\x9999\x828f\x8041\x8006\x8000\x8000\x8000\x8000"[index])
+             << 16u |
+         uint32_t(u"\x999a\x5c29\x8938\x8db9\xa7c6\x10c7\x01ae\x002b"[index]);
 }
 
 template 
@@ -3941,7 +3918,7 @@ template  class generic_context {
 
   constexpr auto out() const -> iterator { return out_; }
 
-  void advance_to(iterator it) {
+  FMT_CONSTEXPR void advance_to(iterator it) {
     if (!detail::is_back_insert_iterator()) out_ = it;
   }
 
diff --git a/include/fmt/os.h b/include/fmt/os.h
index 3fbd6905..a3ee58f3 100644
--- a/include/fmt/os.h
+++ b/include/fmt/os.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - optional OS-specific functionality
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/include/fmt/ostream.h b/include/fmt/ostream.h
index a139e825..da9c65e0 100644
--- a/include/fmt/ostream.h
+++ b/include/fmt/ostream.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - std::ostream support
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/include/fmt/printf.h b/include/fmt/printf.h
index 7f6f6f3a..8d792fbd 100644
--- a/include/fmt/printf.h
+++ b/include/fmt/printf.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - legacy printf implementation
 //
-// Copyright (c) 2012 - 2016, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -330,6 +330,7 @@ template 
 auto parse_header(const Char*& it, const Char* end, format_specs& specs,
                   GetArg get_arg) -> int {
   int arg_index = -1;
+  if (it == end) return arg_index;
   Char c = *it;
   if (c >= '0' && c <= '9') {
     // Parse an argument index (if followed by '$') or a width possibly
@@ -437,6 +438,8 @@ void vprintf(buffer& buf, basic_string_view format,
     }
     write(out, basic_string_view(start, to_unsigned(it - 1 - start)));
 
+    if (it == end) report_error("invalid format string");
+
     auto specs = format_specs();
     specs.set_align(align::right);
 
diff --git a/include/fmt/ranges.h b/include/fmt/ranges.h
index d7dbc168..99bef841 100644
--- a/include/fmt/ranges.h
+++ b/include/fmt/ranges.h
@@ -69,11 +69,12 @@ struct has_member_fn_begin_end_t().begin()),
 
 // Member function overloads.
 template 
-auto range_begin(T&& rng) -> decltype(static_cast(rng).begin()) {
+FMT_CONSTEXPR auto range_begin(T&& rng)
+    -> decltype(static_cast(rng).begin()) {
   return static_cast(rng).begin();
 }
 template 
-auto range_end(T&& rng) -> decltype(static_cast(rng).end()) {
+FMT_CONSTEXPR auto range_end(T&& rng) -> decltype(static_cast(rng).end()) {
   return static_cast(rng).end();
 }
 
@@ -129,6 +130,13 @@ template  class is_tuple_like_ {
       !std::is_void(nullptr))>::value;
 };
 
+template 
+struct is_optional_like_ : std::false_type {};
+template 
+struct is_optional_like_().has_value()),
+                                   decltype(std::declval().value())>>
+    : std::true_type {};
+
 // Check for integer_sequence
 #if defined(__cpp_lib_integer_sequence) || FMT_MSC_VERSION >= 1900
 template 
@@ -343,8 +351,9 @@ struct formatter struct is_range {
-  static constexpr bool value =
-      detail::is_range_::value && !detail::has_to_string_view::value;
+  static constexpr bool value = detail::is_range_::value &&
+                                !detail::is_optional_like_::value &&
+                                !detail::has_to_string_view::value;
 };
 
 namespace detail {
@@ -460,7 +469,8 @@ struct range_formatter<
   }
 
   template 
-  auto format(R&& range, FormatContext& ctx) const -> decltype(ctx.out()) {
+  FMT_CONSTEXPR auto format(R&& range, FormatContext& ctx) const
+      -> decltype(ctx.out()) {
     auto out = ctx.out();
     auto it = detail::range_begin(range);
     auto end = detail::range_end(range);
@@ -516,7 +526,7 @@ struct formatter<
   }
 
   template 
-  auto format(range_type& range, FormatContext& ctx) const
+  FMT_CONSTEXPR auto format(range_type& range, FormatContext& ctx) const
       -> decltype(ctx.out()) {
     return range_formatter_.format(range, ctx);
   }
@@ -625,7 +635,7 @@ struct join_view : detail::view {
   Sentinel end;
   basic_string_view sep;
 
-  join_view(It b, Sentinel e, basic_string_view s)
+  FMT_CONSTEXPR join_view(It b, Sentinel e, basic_string_view s)
       : begin(std::move(b)), end(e), sep(s) {}
 };
 
@@ -652,7 +662,8 @@ struct formatter, Char> {
   }
 
   template 
-  auto format(view& value, FormatContext& ctx) const -> decltype(ctx.out()) {
+  FMT_CONSTEXPR auto format(view& value, FormatContext& ctx) const
+      -> decltype(ctx.out()) {
     using iter =
         conditional_t::value, It, It&>;
     iter it = value.begin;
@@ -675,7 +686,7 @@ template  struct tuple_join_view : detail::view {
   const Tuple& tuple;
   basic_string_view sep;
 
-  tuple_join_view(const Tuple& t, basic_string_view s)
+  FMT_CONSTEXPR tuple_join_view(const Tuple& t, basic_string_view s)
       : tuple(t), sep{s} {}
 };
 
@@ -694,8 +705,9 @@ struct formatter, Char,
   }
 
   template 
-  auto format(const tuple_join_view& value,
-              FormatContext& ctx) const -> typename FormatContext::iterator {
+  FMT_CONSTEXPR auto format(const tuple_join_view& value,
+                            FormatContext& ctx) const ->
+      typename FormatContext::iterator {
     return do_format(value, ctx, std::tuple_size());
   }
 
@@ -726,15 +738,17 @@ struct formatter, Char,
   }
 
   template 
-  auto do_format(const tuple_join_view&, FormatContext& ctx,
-                 std::integral_constant) const ->
+  FMT_CONSTEXPR auto do_format(const tuple_join_view&,
+                               FormatContext& ctx,
+                               std::integral_constant) const ->
       typename FormatContext::iterator {
     return ctx.out();
   }
 
   template 
-  auto do_format(const tuple_join_view& value, FormatContext& ctx,
-                 std::integral_constant) const ->
+  FMT_CONSTEXPR auto do_format(const tuple_join_view& value,
+                               FormatContext& ctx,
+                               std::integral_constant) const ->
       typename FormatContext::iterator {
     using std::get;
     auto out =
@@ -813,7 +827,7 @@ auto join(It begin, Sentinel end, string_view sep) -> join_view {
  *     // Output: 01, 02, 03
  */
 template ::value)>
-auto join(Range&& r, string_view sep)
+FMT_CONSTEXPR auto join(Range&& r, string_view sep)
     -> join_view {
   return {detail::range_begin(r), detail::range_end(r), sep};
diff --git a/include/fmt/std.h b/include/fmt/std.h
index b6b98bb7..379b186e 100644
--- a/include/fmt/std.h
+++ b/include/fmt/std.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - formatters for standard library types
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -133,8 +133,8 @@ void write_escaped_path(basic_memory_buffer& quoted,
 #if defined(__cpp_lib_expected) || FMT_CPP_LIB_VARIANT
 
 template 
-auto write_escaped_alternative(OutputIt out, const T& v, FormatContext& ctx)
-    -> OutputIt {
+FMT_CONSTEXPR auto write_escaped_alternative(OutputIt out, const T& v,
+                                             FormatContext& ctx) -> OutputIt {
   if constexpr (has_to_string_view::value)
     return write_escaped_string(out, detail::to_string_view(v));
   if constexpr (std::is_same_v) return write_escaped_char(out, v);
@@ -508,7 +508,7 @@ template  struct formatter {
   }
 
   template 
-  auto format(const std::monostate&, FormatContext& ctx) const
+  FMT_CONSTEXPR auto format(const std::monostate&, FormatContext& ctx) const
       -> decltype(ctx.out()) {
     return detail::write(ctx.out(), "monostate");
   }
@@ -524,7 +524,7 @@ struct formatter
-  auto format(const Variant& value, FormatContext& ctx) const
+  FMT_CONSTEXPR20 auto format(const Variant& value, FormatContext& ctx) const
       -> decltype(ctx.out()) {
     auto out = ctx.out();
 
@@ -637,15 +637,52 @@ struct formatter<
   template 
   auto format(const std::exception& ex, Context& ctx) const
       -> decltype(ctx.out()) {
-    auto out = ctx.out();
+    return write(ctx.out(), ex);
+  }
+
+ private:
+  template 
+  auto write(OutputIt out, const std::exception& ex) const -> OutputIt {
 #if FMT_USE_RTTI
     if (with_typename_) {
       out = detail::write_demangled_name(out, typeid(ex));
       *out++ = ':';
       *out++ = ' ';
     }
-#endif
-    return detail::write_bytes(out, string_view(ex.what()));
+#endif  // FMT_USE_RTTI
+    out = detail::write_bytes(out, string_view(ex.what()));
+#if FMT_USE_RTTI
+    // If the exception carries a nested exception (e.g. via
+    // std::throw_with_nested), format the whole chain.
+    if (auto* nested = dynamic_cast(&ex)) {
+      if (auto ep = nested->nested_ptr()) {
+        out = detail::write(out, string_view(": "));
+        try {
+          std::rethrow_exception(ep);
+        } catch (const std::exception& nested_ex) {
+          out = write(out, nested_ex);
+        } catch (...) {
+          out = detail::write(out, string_view("unknown exception"));
+        }
+      }
+    }
+#endif  // FMT_USE_RTTI
+    return out;
+  }
+};
+
+template <> struct formatter : formatter {
+  template 
+  auto format(const std::exception_ptr& ep, FormatContext& ctx) const
+      -> decltype(ctx.out()) {
+    if (!ep) return detail::write(ctx.out(), string_view("none"));
+    try {
+      std::rethrow_exception(ep);
+    } catch (const std::exception& e) {
+      return formatter::format(e, ctx);
+    } catch (...) {
+      return detail::write(ctx.out(), string_view("unknown exception"));
+    }
   }
 };
 
@@ -690,11 +727,12 @@ struct formatter
 struct formatter : formatter {
-  static auto format_as(std::byte b) -> unsigned char {
+  FMT_CONSTEXPR static auto format_as(std::byte b) -> unsigned char {
     return static_cast(b);
   }
   template 
-  auto format(std::byte b, Context& ctx) const -> decltype(ctx.out()) {
+  FMT_CONSTEXPR auto format(std::byte b, Context& ctx) const
+      -> decltype(ctx.out()) {
     return formatter::format(format_as(b), ctx);
   }
 };
diff --git a/include/fmt/xchar.h b/include/fmt/xchar.h
index 31706f96..9631372a 100644
--- a/include/fmt/xchar.h
+++ b/include/fmt/xchar.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - optional wchar_t and exotic character support
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -279,10 +279,18 @@ inline auto vformat_to_n(OutputIt out, size_t n, basic_string_view fmt,
   return {buf.out(), buf.count()};
 }
 
+template ::value)>
+FMT_INLINE auto format_to_n(OutputIt out, size_t n, wformat_string fmt,
+                            T&&... args) -> format_to_n_result {
+  return vformat_to_n(out, n, fmt.get(), fmt::make_wformat_args(args...));
+}
+
 template ,
-          FMT_ENABLE_IF(detail::is_output_iterator::value&&
-                            detail::is_exotic_char::value)>
+          FMT_ENABLE_IF(detail::is_output_iterator::value &&
+                        !std::is_same::value &&
+                        !std::is_same::value)>
 inline auto format_to_n(OutputIt out, size_t n, const S& fmt, T&&... args)
     -> format_to_n_result {
   return vformat_to_n(out, n, fmt::basic_string_view(fmt),
diff --git a/src/fmt-c.cc b/src/fmt-c.cc
index cb7dcc1c..d5945ea9 100644
--- a/src/fmt-c.cc
+++ b/src/fmt-c.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - the C API
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -9,12 +9,13 @@
 
 #include 
 
-extern "C" int fmt_vformat(char* buffer, size_t size, const char* fmt,
-                           const fmt_arg* args, size_t num_args) {
-  constexpr size_t max_args = 16;
-  if (num_args > max_args) return fmt_error_invalid_arg;
+constexpr size_t max_c_format_args = 16;
+
+static int convert_c_format_args(
+    fmt::basic_format_arg* format_args,
+    const fmt_arg* args, size_t num_args) {
+  if (num_args > max_c_format_args) return fmt_error_invalid_arg;
 
-  fmt::basic_format_arg format_args[max_args];
   for (size_t i = 0; i < num_args; ++i) {
     switch (args[i].type) {
     case fmt_int:    format_args[i] = args[i].value.int_value; break;
@@ -31,12 +32,36 @@ extern "C" int fmt_vformat(char* buffer, size_t size, const char* fmt,
     default:          return fmt_error_invalid_arg;
     }
   }
-  try {
+  return 0;
+}
+
+extern "C" int fmt_vformat(char* buffer, size_t size, const char* fmt,
+                           const fmt_arg* args, size_t num_args) {
+  fmt::basic_format_arg format_args[max_c_format_args];
+  int error = convert_c_format_args(format_args, args, num_args);
+  if (error != 0) return error;
+
+  FMT_TRY {
     auto result = fmt::vformat_to_n(
         buffer, size, fmt,
         fmt::format_args(format_args, static_cast(num_args)));
     return static_cast(result.size);
-  } catch (...) {
   }
+  FMT_CATCH(...) {}
+  return fmt_error;
+}
+
+extern "C" int fmt_vprint(FILE* stream, const char* fmt, const fmt_arg* args,
+                          size_t num_args) {
+  fmt::basic_format_arg format_args[max_c_format_args];
+  int error = convert_c_format_args(format_args, args, num_args);
+  if (error != 0) return error;
+
+  FMT_TRY {
+    fmt::vprint(stream, fmt,
+                fmt::format_args(format_args, static_cast(num_args)));
+    return 0;
+  }
+  FMT_CATCH(...) {}
   return fmt_error;
 }
diff --git a/src/fmt.cc b/src/fmt.cc
index 1973a1ac..f319ce94 100644
--- a/src/fmt.cc
+++ b/src/fmt.cc
@@ -1,3 +1,10 @@
+// Formatting library for C++ - C++20 module
+//
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
 module;
 
 #define FMT_MODULE
diff --git a/src/format.cc b/src/format.cc
index 526082e3..298c5b4b 100644
--- a/src/format.cc
+++ b/src/format.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++
 //
-// Copyright (c) 2012 - 2016, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/src/os.cc b/src/os.cc
index 409863a9..4840a658 100644
--- a/src/os.cc
+++ b/src/os.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - optional OS-specific functionality
 //
-// Copyright (c) 2012 - 2016, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -13,7 +13,6 @@
 #include "fmt/os.h"
 
 #ifndef FMT_MODULE
-#  include 
 
 #  if FMT_USE_FCNTL
 #    include 
@@ -35,6 +34,8 @@
 
 #  ifdef _WIN32
 #    include 
+
+#    include   // CHAR_BIT
 #  endif
 #endif
 
diff --git a/support/cmake-format-requirements.txt b/support/cmake-format-requirements.txt
new file mode 100644
index 00000000..ee242a8c
--- /dev/null
+++ b/support/cmake-format-requirements.txt
@@ -0,0 +1,6 @@
+cmakelang==0.6.13 \
+    --hash=sha256:764b9467195c7c36453d60a829f30229720d26c7dffd41cb516b99bd9c7daf4e \
+    --hash=sha256:03982e87b00654d024d73ef972d9d9bb0e5726cdb6b8a424a15661fb6278e67f
+six==1.17.0 \
+    --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+    --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
diff --git a/support/doc-requirements.in b/support/doc-requirements.in
new file mode 100644
index 00000000..2ed93929
--- /dev/null
+++ b/support/doc-requirements.in
@@ -0,0 +1,4 @@
+mkdocs-material==9.7.0
+mkdocstrings==1.0.0
+mike==2.1.3
+typing_extensions==4.15.0
diff --git a/support/doc-requirements.txt b/support/doc-requirements.txt
new file mode 100644
index 00000000..4e037774
--- /dev/null
+++ b/support/doc-requirements.txt
@@ -0,0 +1,502 @@
+# Pinned, hash-verified Python dependencies for building the docs.
+# To regenerate, run from the repository root:
+#   uv pip compile support/doc-requirements.in --generate-hashes \
+#     --python-version 3.10 --python-platform x86_64-unknown-linux-gnu \
+#     --output-file support/doc-requirements.txt
+babel==2.18.0 \
+    --hash=sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d \
+    --hash=sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35
+    # via mkdocs-material
+backrefs==7.0 \
+    --hash=sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82 \
+    --hash=sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475 \
+    --hash=sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9 \
+    --hash=sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9 \
+    --hash=sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12 \
+    --hash=sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a
+    # via mkdocs-material
+certifi==2026.5.20 \
+    --hash=sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897 \
+    --hash=sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d
+    # via requests
+charset-normalizer==3.4.7 \
+    --hash=sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc \
+    --hash=sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c \
+    --hash=sha256:07d9e39b01743c3717745f4c530a6349eadbfa043c7577eef86c502c15df2c67 \
+    --hash=sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4 \
+    --hash=sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0 \
+    --hash=sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c \
+    --hash=sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5 \
+    --hash=sha256:12a6fff75f6bc66711b73a2f0addfc4c8c15a20e805146a02d147a318962c444 \
+    --hash=sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153 \
+    --hash=sha256:14265bfe1f09498b9d8ec91e9ec9fa52775edf90fcbde092b25f4a33d444fea9 \
+    --hash=sha256:16d971e29578a5e97d7117866d15889a4a07befe0e87e703ed63cd90cb348c01 \
+    --hash=sha256:177a0ba5f0211d488e295aaf82707237e331c24788d8d76c96c5a41594723217 \
+    --hash=sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b \
+    --hash=sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c \
+    --hash=sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a \
+    --hash=sha256:1dc8b0ea451d6e69735094606991f32867807881400f808a106ee1d963c46a83 \
+    --hash=sha256:1efde3cae86c8c273f1eb3b287be7d8499420cf2fe7585c41d370d3e790054a5 \
+    --hash=sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7 \
+    --hash=sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb \
+    --hash=sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c \
+    --hash=sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1 \
+    --hash=sha256:2cd4a60d0e2fb04537162c62bbbb4182f53541fe0ede35cdf270a1c1e723cc42 \
+    --hash=sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab \
+    --hash=sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df \
+    --hash=sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e \
+    --hash=sha256:320ade88cfb846b8cd6b4ddf5ee9e80ee0c1f52401f2456b84ae1ae6a1a5f207 \
+    --hash=sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18 \
+    --hash=sha256:36836d6ff945a00b88ba1e4572d721e60b5b8c98c155d465f56ad19d68f23734 \
+    --hash=sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38 \
+    --hash=sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110 \
+    --hash=sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18 \
+    --hash=sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44 \
+    --hash=sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d \
+    --hash=sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48 \
+    --hash=sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e \
+    --hash=sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5 \
+    --hash=sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d \
+    --hash=sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53 \
+    --hash=sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790 \
+    --hash=sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c \
+    --hash=sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b \
+    --hash=sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116 \
+    --hash=sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d \
+    --hash=sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10 \
+    --hash=sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6 \
+    --hash=sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2 \
+    --hash=sha256:6370e8686f662e6a3941ee48ed4742317cafbe5707e36406e9df792cdb535776 \
+    --hash=sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a \
+    --hash=sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265 \
+    --hash=sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008 \
+    --hash=sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943 \
+    --hash=sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374 \
+    --hash=sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246 \
+    --hash=sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e \
+    --hash=sha256:6e0d51f618228538a3e8f46bd246f87a6cd030565e015803691603f55e12afb5 \
+    --hash=sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616 \
+    --hash=sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15 \
+    --hash=sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41 \
+    --hash=sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960 \
+    --hash=sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752 \
+    --hash=sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e \
+    --hash=sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72 \
+    --hash=sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7 \
+    --hash=sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8 \
+    --hash=sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b \
+    --hash=sha256:813c0e0132266c08eb87469a642cb30aaff57c5f426255419572aaeceeaa7bf4 \
+    --hash=sha256:82b271f5137d07749f7bf32f70b17ab6eaabedd297e75dce75081a24f76eb545 \
+    --hash=sha256:84c018e49c3bf790f9c2771c45e9313a08c2c2a6342b162cd650258b57817706 \
+    --hash=sha256:8751d2787c9131302398b11e6c8068053dcb55d5a8964e114b6e196cf16cb366 \
+    --hash=sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb \
+    --hash=sha256:87fad7d9ba98c86bcb41b2dc8dbb326619be2562af1f8ff50776a39e55721c5a \
+    --hash=sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e \
+    --hash=sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00 \
+    --hash=sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f \
+    --hash=sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a \
+    --hash=sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1 \
+    --hash=sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66 \
+    --hash=sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356 \
+    --hash=sha256:a6c5863edfbe888d9eff9c8b8087354e27618d9da76425c119293f11712a6319 \
+    --hash=sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4 \
+    --hash=sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad \
+    --hash=sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d \
+    --hash=sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5 \
+    --hash=sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7 \
+    --hash=sha256:aef65cd602a6d0e0ff6f9930fcb1c8fec60dd2cfcb6facaf4bdb0e5873042db0 \
+    --hash=sha256:af21eb4409a119e365397b2adbaca4c9ccab56543a65d5dbd9f920d6ac29f686 \
+    --hash=sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34 \
+    --hash=sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49 \
+    --hash=sha256:bb8cc7534f51d9a017b93e3e85b260924f909601c3df002bcdb58ddb4dc41a5c \
+    --hash=sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1 \
+    --hash=sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e \
+    --hash=sha256:bd9b23791fe793e4968dba0c447e12f78e425c59fc0e3b97f6450f4781f3ee60 \
+    --hash=sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0 \
+    --hash=sha256:c0f081d69a6e58272819b70288d3221a6ee64b98df852631c80f293514d3b274 \
+    --hash=sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d \
+    --hash=sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0 \
+    --hash=sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae \
+    --hash=sha256:c593052c465475e64bbfe5dbd81680f64a67fdc752c56d7a0ae205dc8aeefe0f \
+    --hash=sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d \
+    --hash=sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe \
+    --hash=sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3 \
+    --hash=sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393 \
+    --hash=sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1 \
+    --hash=sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af \
+    --hash=sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44 \
+    --hash=sha256:d61f00a0869d77422d9b2aba989e2d24afa6ffd552af442e0e58de4f35ea6d00 \
+    --hash=sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c \
+    --hash=sha256:dca4bbc466a95ba9c0234ef56d7dd9509f63da22274589ebd4ed7f1f4d4c54e3 \
+    --hash=sha256:dd915403e231e6b1809fe9b6d9fc55cf8fb5e02765ac625d9cd623342a7905d7 \
+    --hash=sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd \
+    --hash=sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e \
+    --hash=sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b \
+    --hash=sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8 \
+    --hash=sha256:e5f4d355f0a2b1a31bc3edec6795b46324349c9cb25eed068049e4f472fb4259 \
+    --hash=sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859 \
+    --hash=sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46 \
+    --hash=sha256:e80c8378d8f3d83cd3164da1ad2df9e37a666cdde7b1cb2298ed0b558064be30 \
+    --hash=sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b \
+    --hash=sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46 \
+    --hash=sha256:ed065083d0898c9d5b4bbec7b026fd755ff7454e6e8b73a67f8c744b13986e24 \
+    --hash=sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a \
+    --hash=sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24 \
+    --hash=sha256:f22dec1690b584cea26fade98b2435c132c1b5f68e39f5a0b7627cd7ae31f1dc \
+    --hash=sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215 \
+    --hash=sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063 \
+    --hash=sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832 \
+    --hash=sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6 \
+    --hash=sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79 \
+    --hash=sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464
+    # via requests
+click==8.4.1 \
+    --hash=sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2 \
+    --hash=sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96
+    # via mkdocs
+colorama==0.4.6 \
+    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
+    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
+    # via mkdocs-material
+ghp-import==2.1.0 \
+    --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
+    --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
+    # via mkdocs
+idna==3.17 \
+    --hash=sha256:466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c \
+    --hash=sha256:5eb0cb53bc467c12eadcf6de83163ad8527cec9416f44b9b61b19caedad2b87f
+    # via requests
+importlib-metadata==9.0.0 \
+    --hash=sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7 \
+    --hash=sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc
+    # via mike
+importlib-resources==7.1.0 \
+    --hash=sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708 \
+    --hash=sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1
+    # via mike
+jinja2==3.1.6 \
+    --hash=sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d \
+    --hash=sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67
+    # via
+    #   mike
+    #   mkdocs
+    #   mkdocs-material
+    #   mkdocstrings
+markdown==3.10.2 \
+    --hash=sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950 \
+    --hash=sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36
+    # via
+    #   mkdocs
+    #   mkdocs-autorefs
+    #   mkdocs-material
+    #   mkdocstrings
+    #   pymdown-extensions
+markupsafe==3.0.3 \
+    --hash=sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f \
+    --hash=sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a \
+    --hash=sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf \
+    --hash=sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19 \
+    --hash=sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf \
+    --hash=sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c \
+    --hash=sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175 \
+    --hash=sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219 \
+    --hash=sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb \
+    --hash=sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6 \
+    --hash=sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab \
+    --hash=sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26 \
+    --hash=sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1 \
+    --hash=sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce \
+    --hash=sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218 \
+    --hash=sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634 \
+    --hash=sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695 \
+    --hash=sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad \
+    --hash=sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73 \
+    --hash=sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c \
+    --hash=sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe \
+    --hash=sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa \
+    --hash=sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559 \
+    --hash=sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa \
+    --hash=sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37 \
+    --hash=sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758 \
+    --hash=sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f \
+    --hash=sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8 \
+    --hash=sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d \
+    --hash=sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c \
+    --hash=sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97 \
+    --hash=sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a \
+    --hash=sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19 \
+    --hash=sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9 \
+    --hash=sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9 \
+    --hash=sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc \
+    --hash=sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2 \
+    --hash=sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4 \
+    --hash=sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354 \
+    --hash=sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50 \
+    --hash=sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698 \
+    --hash=sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9 \
+    --hash=sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b \
+    --hash=sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc \
+    --hash=sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115 \
+    --hash=sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e \
+    --hash=sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485 \
+    --hash=sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f \
+    --hash=sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12 \
+    --hash=sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025 \
+    --hash=sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009 \
+    --hash=sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d \
+    --hash=sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b \
+    --hash=sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a \
+    --hash=sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5 \
+    --hash=sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f \
+    --hash=sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d \
+    --hash=sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1 \
+    --hash=sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287 \
+    --hash=sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6 \
+    --hash=sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f \
+    --hash=sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581 \
+    --hash=sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed \
+    --hash=sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b \
+    --hash=sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c \
+    --hash=sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026 \
+    --hash=sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8 \
+    --hash=sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676 \
+    --hash=sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6 \
+    --hash=sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e \
+    --hash=sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d \
+    --hash=sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d \
+    --hash=sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01 \
+    --hash=sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7 \
+    --hash=sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419 \
+    --hash=sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795 \
+    --hash=sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1 \
+    --hash=sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5 \
+    --hash=sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d \
+    --hash=sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42 \
+    --hash=sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe \
+    --hash=sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda \
+    --hash=sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e \
+    --hash=sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737 \
+    --hash=sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523 \
+    --hash=sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591 \
+    --hash=sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc \
+    --hash=sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a \
+    --hash=sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50
+    # via
+    #   jinja2
+    #   mkdocs
+    #   mkdocs-autorefs
+    #   mkdocstrings
+mergedeep==1.3.4 \
+    --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \
+    --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307
+    # via
+    #   mkdocs
+    #   mkdocs-get-deps
+mike==2.1.3 \
+    --hash=sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810 \
+    --hash=sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a
+    # via -r /tmp/doc-requirements.in
+mkdocs==1.6.1 \
+    --hash=sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2 \
+    --hash=sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e
+    # via
+    #   mike
+    #   mkdocs-autorefs
+    #   mkdocs-material
+    #   mkdocstrings
+mkdocs-autorefs==1.4.4 \
+    --hash=sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089 \
+    --hash=sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197
+    # via mkdocstrings
+mkdocs-get-deps==0.2.2 \
+    --hash=sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1 \
+    --hash=sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650
+    # via mkdocs
+mkdocs-material==9.7.0 \
+    --hash=sha256:602b359844e906ee402b7ed9640340cf8a474420d02d8891451733b6b02314ec \
+    --hash=sha256:da2866ea53601125ff5baa8aa06404c6e07af3c5ce3d5de95e3b52b80b442887
+    # via -r /tmp/doc-requirements.in
+mkdocs-material-extensions==1.3.1 \
+    --hash=sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443 \
+    --hash=sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31
+    # via mkdocs-material
+mkdocstrings==1.0.0 \
+    --hash=sha256:351a006dbb27aefce241ade110d3cd040c1145b7a3eb5fd5ac23f03ed67f401a \
+    --hash=sha256:4c50eb960bff6e05dfc631f6bc00dfabffbcb29c5ff25f676d64daae05ed82fa
+    # via -r /tmp/doc-requirements.in
+packaging==26.2 \
+    --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
+    --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
+    # via mkdocs
+paginate==0.5.7 \
+    --hash=sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945 \
+    --hash=sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591
+    # via mkdocs-material
+pathspec==1.1.1 \
+    --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \
+    --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189
+    # via mkdocs
+platformdirs==4.10.0 \
+    --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \
+    --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a
+    # via mkdocs-get-deps
+pygments==2.20.0 \
+    --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \
+    --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176
+    # via mkdocs-material
+pymdown-extensions==10.21.3 \
+    --hash=sha256:72cfcf55f07aea0d4af2c4f11dd4e52466ddfb1bb819673146398e0bd3a77354 \
+    --hash=sha256:d7a5d08014fc571e80ca21dd6f854e31f94c489800350564d55d15b3c41e76b6
+    # via
+    #   mkdocs-material
+    #   mkdocstrings
+pyparsing==3.3.2 \
+    --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \
+    --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc
+    # via mike
+python-dateutil==2.9.0.post0 \
+    --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \
+    --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427
+    # via ghp-import
+pyyaml==6.0.3 \
+    --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
+    --hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
+    --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
+    --hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
+    --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
+    --hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
+    --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
+    --hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
+    --hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
+    --hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
+    --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
+    --hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
+    --hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
+    --hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
+    --hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
+    --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
+    --hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
+    --hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
+    --hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
+    --hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
+    --hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
+    --hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
+    --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
+    --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
+    --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
+    --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
+    --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
+    --hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
+    --hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
+    --hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
+    --hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
+    --hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
+    --hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
+    --hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
+    --hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
+    --hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
+    --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
+    --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
+    --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
+    --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
+    --hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
+    --hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
+    --hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
+    --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
+    --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
+    --hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
+    --hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
+    --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
+    --hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
+    --hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
+    --hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
+    --hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
+    --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
+    --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
+    --hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
+    --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
+    --hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
+    --hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
+    --hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
+    --hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
+    --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
+    --hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
+    --hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
+    --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
+    --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
+    --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
+    --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
+    --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
+    --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
+    --hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
+    --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
+    --hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
+    --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
+    # via
+    #   mike
+    #   mkdocs
+    #   mkdocs-get-deps
+    #   pymdown-extensions
+    #   pyyaml-env-tag
+pyyaml-env-tag==1.1 \
+    --hash=sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04 \
+    --hash=sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff
+    # via
+    #   mike
+    #   mkdocs
+requests==2.34.2 \
+    --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
+    --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
+    # via mkdocs-material
+six==1.17.0 \
+    --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \
+    --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81
+    # via python-dateutil
+typing-extensions==4.15.0 \
+    --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \
+    --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548
+    # via -r /tmp/doc-requirements.in
+urllib3==2.7.0 \
+    --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
+    --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
+    # via requests
+verspec==0.1.0 \
+    --hash=sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31 \
+    --hash=sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e
+    # via mike
+watchdog==6.0.0 \
+    --hash=sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a \
+    --hash=sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2 \
+    --hash=sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f \
+    --hash=sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c \
+    --hash=sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c \
+    --hash=sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c \
+    --hash=sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0 \
+    --hash=sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13 \
+    --hash=sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134 \
+    --hash=sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa \
+    --hash=sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e \
+    --hash=sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379 \
+    --hash=sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a \
+    --hash=sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11 \
+    --hash=sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282 \
+    --hash=sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b \
+    --hash=sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f \
+    --hash=sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c \
+    --hash=sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112 \
+    --hash=sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948 \
+    --hash=sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881 \
+    --hash=sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860 \
+    --hash=sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3 \
+    --hash=sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680 \
+    --hash=sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26 \
+    --hash=sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26 \
+    --hash=sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e \
+    --hash=sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8 \
+    --hash=sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c \
+    --hash=sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2
+    # via mkdocs
+zipp==4.1.0 \
+    --hash=sha256:25ad4e16390cd314347dd8f1de67a2ac538ae658ed4ab9db16029c07c188e97f \
+    --hash=sha256:4cb57381f544315db7688e976e922a2b18cdb513d21cc194eb42232ba2a3e602
+    # via importlib-metadata
diff --git a/support/llvm-snapshot.gpg.key b/support/llvm-snapshot.gpg.key
new file mode 100644
index 00000000..27f27e13
--- /dev/null
+++ b/support/llvm-snapshot.gpg.key
@@ -0,0 +1,52 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+mQINBFE9lCwBEADi0WUAApM/mgHJRU8lVkkw0CHsZNpqaQDNaHefD6Rw3S4LxNmM
+EZaOTkhP200XZM8lVdbfUW9xSjA3oPldc1HG26NjbqqCmWpdo2fb+r7VmU2dq3NM
+R18ZlKixiLDE6OUfaXWKamZsXb6ITTYmgTO6orQWYrnW6ckYHSeaAkW0wkDAryl2
+B5v8aoFnQ1rFiVEMo4NGzw4UX+MelF7rxaaregmKVTPiqCOSPJ1McC1dHFN533FY
+Wh/RVLKWo6npu+owtwYFQW+zyQhKzSIMvNujFRzhIxzxR9Gn87MoLAyfgKEzrbbT
+DhqqNXTxS4UMUKCQaO93TzetX/EBrRpJj+vP640yio80h4Dr5pAd7+LnKwgpTDk1
+G88bBXJAcPZnTSKu9I2c6KY4iRNbvRz4i+ZdwwZtdW4nSdl2792L7Sl7Nc44uLL/
+ZqkKDXEBF6lsX5XpABwyK89S/SbHOytXv9o4puv+65Ac5/UShspQTMSKGZgvDauU
+cs8kE1U9dPOqVNCYq9Nfwinkf6RxV1k1+gwtclxQuY7UpKXP0hNAXjAiA5KS5Crq
+7aaJg9q2F4bub0mNU6n7UI6vXguF2n4SEtzPRk6RP+4TiT3bZUsmr+1ktogyOJCc
+Ha8G5VdL+NBIYQthOcieYCBnTeIH7D3Sp6FYQTYtVbKFzmMK+36ERreL/wARAQAB
+tD1TeWx2ZXN0cmUgTGVkcnUgLSBEZWJpYW4gTExWTSBwYWNrYWdlcyA8c3lsdmVz
+dHJlQGRlYmlhbi5vcmc+iQJMBBMBCgA2AhsDAh4BAheABAsJCAcEFQoJCAUWAgMB
+ABYhBGCE88+BS1fBzxLv1RXPTRivT3QhBQJpl4k7AAoJEBXPTRivT3QhivEP/2Ix
+/Pi72KhD47zbxLecS0VLB/gURPC2h5AgWgW5P/GSQe0QXHAkqCWFd9y1Y5PI8e3v
+6RZFDCPJrfVKfHzIx4nciUHr0qlJqGaplvf6AZvgvF8Xrua9VZaHnZJO+GmK+WtH
+/bCgtzk91O08w8wcZ/4wmopQjcSu69OcpqpPBVClhIxaIIFFlATdUAKNVAzXDQDf
+WfWT2N3ghsbac1MRQKKzddzXgvVw5mRAw0NCKUJ2DPSohfRsS1LjVG4wThkLWZJb
+tPx6+meunBspvvROthh+HEEjiB5o4i7230AOhyYAOjgZj3wQq7A/2O0Ro1A2zl5L
+vB17a6WLnEdUzLgj0NYtE1x4W0fNAspEQpIXvVRkZzHA+GH31a2Fs/S2kqsuF/OY
+hsD35d/6lJtxbNY2sYsprDxT2tfdnUG9KCJSI3p/vjGIezFgrfBB7u6lUHx/GAQM
+nwpiXKeRRS4fT03aZ79FR3xVE7pLOHRq4tUVEH1lvHQB8aU07NHK61BR8NXS++cg
+LhveD+TwCKNQ9WaDdNNDl0jGbOBzcou+KguZD8wfoCj/Ko0CdCUFdO+H+TPsIH4F
+nRovkQ5bN7Y7iTHiV96mDOtnE2kd4ctbioEl8/4zroKCZTQEzJRQ/Zz+NdtRa/ev
+n2jUyoHWs2Vc5bYLyvwjk6CCurIumdT6JAdEUMF6uQINBFE9lCwBEADiPFiZtMfC
+YyOpKLOUwlg24Y6kiJ0R3H3NDmDM6vjfX9V9arhL5WEwOGzXmP2XKaXTLiiTbXlV
+EXCsBEQODpQ8yvtqMuYFAZq7QVxu9kn3SXg2NenmnQ0QT+o5v1QQdPsQaQSFpnV6
+SRYVNYKCzLutFd9kvEHqyTunMsIOyHJbfOMhqRYCelDmPmhuIPLvTylF4C4YH3g1
+LJvUehb/EawaC+8LDivTDfE9K3AXU+GZJwTo1MEh8uWJ5remXy+Eo+UKHngeTsjx
+AaOjUROVE/hCWXVnEcny7bl/+1pERLYDDweLuPQRVxnoxP0lZMjzfJRydSVk9lLY
+fJ5ZxSP7ydQLxbg3wCf5o2mz2X/bWKciXZNsajdifUlAucDVgKgbALfiDxBlMmES
+URAE3Rfb1rJZbo50XHwwO+WVsFXKaYqraUrVTnmZBN8OWLtLe98bP0an+hDBnxtC
+k55SRzmFtmzNjA/hFf7rzEM9+f4z252X6pb3C74vJ49tbodf3QuaSE2SRjZnNQ9E
+8MdkA1Tlu1o9dzYft51fjLiZpBrF6eoPSUaH0sbP+amzgcRm0IjkJjyjS4IwXeVP
+Rd9eG4uSyvs/G/nvK57wstMi04l30ebOn0b3tJFhh6pT7yXI1e1TY92ssurtAeTO
+uyWgbvyUKlBxqUcYAInOuVRZU/983ce3eQARAQABiQI2BBgBCgAgAhsMFiEEYITz
+z4FLV8HPEu/VFc9NGK9PdCEFAmmXid0ACgkQFc9NGK9PdCF/PxAAlMhoX2YtQ2vc
+aPt+6hZ3q/8y+xht/WK7YmPm76b7tmRvahZIIfr9Criu9HyL4nFXlVnYot63qIqa
+ebu4GUr7Dau+oimJ2olLXMbSjhg7R/Q9ESqG/6pvBl6gwgKqxQAh8lVgK4KevK++
+nxdPLLNe9F1kkwnLKCUvYElEYEUMrqI4GMHfgPw84vCCWmR8CSZ+AJ95tft5Xzxf
+7uENFADYkDkLiA4giHSasMOhiVxhZWSI+UVJ2vTGuscma509kqs+CjzXOzCKymQ4
+qVCjKLaSkQK4rUV125AU0WfRFpaKtiHWmLic5opBzCGhvJ7HI8iOxAFq3bHB1WN1
+Ba7VHOCbSKAFIw+7n+onEmKPpiZe55pc684wHy1RD3YnimdJWyHxMXSGuhggv3W1
+CWxE1U2fmnMzXbn3T1VXk5/q/5X4wtirzWobZIzVcg5WcoSx0w0LOqIgf6C1z0jq
+FLloorOrbdfYOg6GEhaoiiYiwnWwPiKHNqONpMqXitwh/lCK3SomtdsACy1FlVP7
+HvM41+jf7qZ9WqCvUZKc+11XAUdOaqAoxQU/T1I1gPGTTJ3uYvAsOqQc9SZAt3Io
+8QkYjS9LQvxtZqAdWUALaiQIYKf3uJE5eWP3sF6uEJA3LTyIZsMd7MO25UEPLKCw
+O9BuqGhXJW1iyMZ/Smhp993FdBBf4FA=
+=/NY6
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/support/mkdocs b/support/mkdocs
index 94eddae3..e595a2be 100755
--- a/support/mkdocs
+++ b/support/mkdocs
@@ -71,15 +71,18 @@ if len(args) > 0:
     # mike stages files added by deploy for deletion for unclear reason,
     # undo it.
     ret = call(['git', 'reset', '--hard'], cwd=site_dir)
-    if False:
-      os.makedirs(current_doc_path, exist_ok=True)
-      redirect_page_path = os.path.join(current_doc_path, 'api.html')
-      with open(redirect_page_path, "w") as file:
-        file.write(redirect_page)
-      ret = call(['git', 'add', redirect_page_path], cwd=site_dir)
-      if ret != 0:
-        sys.exit(ret)
-      ret = call(['git', 'commit', '--amend', '--no-edit'], cwd=site_dir)
+    if ret != 0:
+      sys.exit(ret)
+    # Generate a redirect page from the old Sphinx api.html to the new api/
+    # page, preserving the URL hash so links such as api.html#udt keep working.
+    os.makedirs(current_doc_path, exist_ok=True)
+    redirect_page_path = os.path.join(current_doc_path, 'api.html')
+    with open(redirect_page_path, "w") as file:
+      file.write(redirect_page)
+    ret = call(['git', 'add', redirect_page_path], cwd=site_dir)
+    if ret != 0:
+      sys.exit(ret)
+    ret = call(['git', 'commit', '--amend', '--no-edit'], cwd=site_dir)
     sys.exit(ret)
   elif not command.startswith('-'):
     args += ['-f', config_path]
diff --git a/support/release.py b/support/release.py
index 26de7f4f..b3585b84 100755
--- a/support/release.py
+++ b/support/release.py
@@ -35,6 +35,9 @@ class Git:
     def clone(self, *args):
         return self.call('clone', list(args) + [self.dir])
 
+    def fetch(self, *args):
+        return self.call('fetch', args, cwd=self.dir)
+
     def commit(self, *args):
         return self.call('commit', args, cwd=self.dir)
 
@@ -56,16 +59,22 @@ class Git:
 
 def clean_checkout(repo, branch):
     repo.clean('-f', '-d')
-    repo.reset('--hard')
+    repo.fetch('origin')
     repo.checkout(branch)
+    # Hard-reset to the remote so a reused clone picks up new commits
+    # instead of building from stale local state.
+    repo.reset('--hard', 'origin/' + branch)
 
 
 class Runner:
-    def __init__(self, cwd):
+    def __init__(self, cwd, env=None):
         self.cwd = cwd
+        self.env = env
 
     def __call__(self, *args, **kwargs):
         kwargs['cwd'] = kwargs.get('cwd', self.cwd)
+        if self.env is not None:
+            kwargs['env'] = kwargs.get('env', self.env)
         check_call(args, **kwargs)
 
 
@@ -80,6 +89,28 @@ def create_build_env():
     return env
 
 
+def create_doc_env(env, fmt_repo):
+    """Create a virtualenv with the pinned documentation dependencies and
+    return an environment dict with it prepended to PATH. This ensures the
+    docs are built with the exact mkdocs/mkdocstrings versions required by the
+    custom handler, regardless of what is installed system-wide."""
+    # Use an absolute path so the venv resolves on PATH regardless of the
+    # working directory the build steps run in.
+    venv_dir = os.path.abspath(os.path.join(env.build_dir, 'venv'))
+    shutil.rmtree(venv_dir, ignore_errors=True)
+    check_call([sys.executable, '-m', 'venv', venv_dir])
+    venv_bin = os.path.join(venv_dir, 'bin')
+    pip = os.path.join(venv_bin, 'pip')
+    check_call([pip, 'install', '--quiet', '--upgrade', 'pip'])
+    requirements = os.path.join(
+        fmt_repo.dir, 'support', 'doc-requirements.txt')
+    check_call(
+        [pip, 'install', '--quiet', '--require-hashes', '-r', requirements])
+    doc_env = os.environ.copy()
+    doc_env['PATH'] = venv_bin + os.pathsep + doc_env.get('PATH', '')
+    return doc_env
+
+
 if __name__ == '__main__':
     args = docopt.docopt(__doc__)
     env = create_build_env()
@@ -87,7 +118,7 @@ if __name__ == '__main__':
 
     branch = args.get('')
     if branch is None:
-        branch = 'master'
+        branch = 'main'
     if not fmt_repo.update('-b', branch, 'git@github.com:fmtlib/fmt'):
         clean_checkout(fmt_repo, branch)
 
@@ -151,12 +182,21 @@ if __name__ == '__main__':
     fmt_repo.add(changelog)
     fmt_repo.commit('-m', 'Update version')
 
-    # Build the docs and package.
-    run = Runner(fmt_repo.dir)
+    # Build the docs locally in a virtualenv with the pinned doc dependencies;
+    # the source zip is now built and attached to the release in CI by
+    # .github/workflows/release.yml, which also generates a SLSA provenance
+    # attestation for it. The venv is prepended to PATH so that CMake's
+    # find_program(MKDOCS mkdocs) and the ./mkdocs deploy step below both pick
+    # up the correct mkdocs/mkdocstrings versions.
+    doc_env = create_doc_env(env, fmt_repo)
+    run = Runner(fmt_repo.dir, env=doc_env)
     run('cmake', '.')
-    run('make', 'doc', 'package_source')
+    run('make', 'doc')
 
-    # Create a release on GitHub.
+    # Create a draft release on GitHub, then trigger the release workflow to
+    # build the source zip from the `release` branch and attach the zip plus
+    # *.intoto.jsonl provenance to this draft. After reviewing the draft, the
+    # maintainer clicks Publish to finalize.
     fmt_repo.push('origin', 'release')
     auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')}
     req = urllib.request.Request(
@@ -169,20 +209,21 @@ if __name__ == '__main__':
         if response.status != 201:
             raise Exception(f'Failed to create a release ' +
                             '{response.status} {response.reason}')
-        response_data = json.loads(response.read().decode('utf-8'))
-        id = response_data['id']
 
-    # Upload the package.
-    uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases'
-    package = 'fmt-{}.zip'.format(version)
-    req = urllib.request.Request(
-        f'{uploads_url}/{id}/assets?name={package}',
-        headers={'Content-Type': 'application/zip'} | auth_headers,
-        data=open('build/fmt/' + package, 'rb').read(), method='POST')
-    with urllib.request.urlopen(req) as response:
-        if response.status != 201:
-            raise Exception(f'Failed to upload an asset '
-                            '{response.status} {response.reason}')
+    # Draft releases do not fire `release: created`, so explicitly dispatch the
+    # release workflow. It runs from the default branch (`branch`) but builds
+    # and uploads artifacts from the `release` branch for tag `version`.
+    dispatch_req = urllib.request.Request(
+        'https://api.github.com/repos/fmtlib/fmt/actions/workflows/'
+        'release.yml/dispatches',
+        data=json.dumps({'ref': branch,
+                         'inputs': {'tag_name': version,
+                                    'ref': 'release'}}).encode('utf-8'),
+        headers=auth_headers, method='POST')
+    with urllib.request.urlopen(dispatch_req) as response:
+        if response.status != 204:
+            raise Exception('Failed to dispatch the release workflow ' +
+                            f'{response.status} {response.reason}')
 
     short_version = '.'.join(version.split('.')[:-1])
-    check_call(['./mkdocs', 'deploy', short_version])
+    check_call(['./mkdocs', 'deploy', short_version], env=doc_env)
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
index a149241b..cd7ba177 100644
--- a/test/CMakeLists.txt
+++ b/test/CMakeLists.txt
@@ -19,7 +19,7 @@ function (add_fmt_test name)
       set(PEDANTIC_COMPILE_FLAGS ${PEDANTIC_COMPILE_FLAGS} -Wno-weak-vtables)
     endif ()
   elseif (ADD_FMT_TEST_MODULE)
-    set(libs test-main)
+    set(libs test-main fmt-module)
   else ()
     set(libs test-main fmt)
   endif ()
@@ -81,9 +81,7 @@ add_executable(perf-sanity perf-sanity.cc)
 target_link_libraries(perf-sanity fmt::fmt)
 
 if (FMT_MODULE)
-  # Module tests are currently disabled.
-  # add_fmt_test(module-test MODULE test-main.cc)
-  # target_link_libraries(module-test fmt-module)
+  add_fmt_test(module-test MODULE)
 endif ()
 
 if (NOT DEFINED MSVC_STATIC_RUNTIME AND MSVC)
diff --git a/test/add-subdirectory-test/CMakeLists.txt b/test/add-subdirectory-test/CMakeLists.txt
index 362b0494..65be2525 100644
--- a/test/add-subdirectory-test/CMakeLists.txt
+++ b/test/add-subdirectory-test/CMakeLists.txt
@@ -15,3 +15,12 @@ if (TARGET fmt::fmt-header-only)
   target_compile_options(header-only-test PRIVATE ${PEDANTIC_COMPILE_FLAGS})
   target_link_libraries(header-only-test fmt::fmt-header-only)
 endif ()
+
+# Test that a target publicly depending on fmt can itself be exported even
+# though fmt is added via add_subdirectory and FMT_INSTALL is off (#4806).
+add_library(export-test STATIC export-lib.cc)
+target_link_libraries(export-test PUBLIC fmt::fmt)
+export(
+  TARGETS export-test
+  NAMESPACE test::
+  FILE ${CMAKE_CURRENT_BINARY_DIR}/export-test-targets.cmake)
diff --git a/test/add-subdirectory-test/export-lib.cc b/test/add-subdirectory-test/export-lib.cc
new file mode 100644
index 00000000..e8adbb1c
--- /dev/null
+++ b/test/add-subdirectory-test/export-lib.cc
@@ -0,0 +1,3 @@
+#include "fmt/base.h"
+
+void greet(const char* name) { fmt::print("Hello, {}!\n", name); }
diff --git a/test/args-test.cc b/test/args-test.cc
index dd0e57dc..8e9f53e5 100644
--- a/test/args-test.cc
+++ b/test/args-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - dynamic argument store tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/assert-test.cc b/test/assert-test.cc
index 368fb587..e0588184 100644
--- a/test/assert-test.cc
+++ b/test/assert-test.cc
@@ -4,7 +4,7 @@
 // which are slow on some platforms. In other tests FMT_ASSERT is made to throw
 // an exception which is much faster and easier to check.
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/base-test.cc b/test/base-test.cc
index bf268875..837c4e5e 100644
--- a/test/base-test.cc
+++ b/test/base-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - core tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -21,8 +21,10 @@
 #include    // std::equal_to
 #include      // std::back_insert_iterator, std::distance
 #include        // std::numeric_limits
+#include          // std::list
 #include        // std::string
 #include   // std::is_same
+#include        // std::vector
 
 #include "gmock/gmock.h"
 
@@ -811,10 +813,10 @@ TEST(base_test, throw_in_buffer_dtor) {
   constexpr int buffer_size = 256;
 
   struct throwing_iterator {
-    int& count;
+    int* count;
 
     auto operator=(char) -> throwing_iterator& {
-      if (++count > buffer_size) throw std::exception();
+      if (++*count > buffer_size) throw std::exception();
       return *this;
     }
     auto operator*() -> throwing_iterator& { return *this; }
@@ -824,7 +826,7 @@ TEST(base_test, throw_in_buffer_dtor) {
 
   try {
     int count = 0;
-    fmt::format_to(throwing_iterator{count}, fmt::runtime("{:{}}{"), "",
+    fmt::format_to(throwing_iterator{&count}, fmt::runtime("{:{}}{"), "",
                    buffer_size + 1);
   } catch (const std::exception&) {
   }
@@ -872,10 +874,32 @@ struct custom_container {
   auto operator[](size_t) -> char& { return data; }
 };
 
+struct phantom_subscript_container {
+  using value_type = char;
+
+  auto data() -> char*;
+  auto size() const -> size_t;
+  operator const char*() const;
+};
+
+struct real_subscript_container : phantom_subscript_container {
+  auto operator[](size_t) -> char&;
+};
+
 FMT_BEGIN_NAMESPACE
 template <> struct is_contiguous : std::true_type {};
 FMT_END_NAMESPACE
 
+TEST(base_test, is_contiguous) {
+  EXPECT_TRUE((fmt::is_contiguous::value));
+  EXPECT_TRUE((fmt::is_contiguous::value));
+  EXPECT_TRUE((fmt::is_contiguous::value));
+  EXPECT_TRUE((fmt::is_contiguous>::value));
+  EXPECT_FALSE((fmt::is_contiguous>::value));
+  EXPECT_FALSE((fmt::is_contiguous::value));
+  EXPECT_TRUE((fmt::is_contiguous::value));
+}
+
 TEST(base_test, format_to_custom_container) {
   auto c = custom_container();
   fmt::format_to(std::back_inserter(c), "");
diff --git a/test/c-test.c b/test/c-test.c
index 71056841..db7c8360 100644
--- a/test/c-test.c
+++ b/test/c-test.c
@@ -1,6 +1,6 @@
 // Formatting library for C++ - the C API tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -83,10 +83,30 @@ void test_buffer_size_query(void) {
   ASSERT_INT_EQ(size, 15);
 }
 
+void test_print(void) {
+  FILE* file = tmpfile();
+  if (!file) {
+    fprintf(stderr, "\nFailed to create temporary file\n");
+    exit(1);
+  }
+
+  int ret = fmt_print(file, "{} and {}", 42, "foo");
+  ASSERT_INT_EQ(ret, 0);
+
+  rewind(file);
+  char buf[100];
+  size_t n = fread(buf, 1, sizeof(buf) - 1, file);
+  buf[n] = '\0';
+  ASSERT_STR_EQ(buf, "42 and foo");
+
+  fclose(file);
+}
+
 int main(void) {
   printf("Running C API tests\n");
   test_types();
   test_zero_arguments();
   test_buffer_size_query();
+  test_print();
   printf("C API tests passed\n");
 }
diff --git a/test/chrono-test.cc b/test/chrono-test.cc
index 98b37000..b7a4e59a 100644
--- a/test/chrono-test.cc
+++ b/test/chrono-test.cc
@@ -1,13 +1,12 @@
 // Formatting library for C++ - time formatting tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
 #include "fmt/chrono.h"
 
-#include 
 #include 
 #include 
 
@@ -359,6 +358,9 @@ TEST(chrono_test, tm) {
   char tz[] = "EET";
   if (fmt::detail::set_tm_zone(time, tz)) {
     EXPECT_EQ(fmt::format(fmt::runtime("{:%Z}"), time), "EET");
+    fmt::detail::set_tm_zone(time, nullptr);
+    EXPECT_THROW_MSG((void)fmt::format(fmt::runtime("{:%Z}"), time),
+                     fmt::format_error, "no timezone");
   } else {
     EXPECT_THROW_MSG((void)fmt::format(fmt::runtime("{:%Z}"), time),
                      fmt::format_error, "no timezone");
@@ -987,6 +989,9 @@ TEST(chrono_test, glibc_extensions) {
 TEST(chrono_test, out_of_range) {
   auto d = std::chrono::duration(538976288);
   EXPECT_THROW((void)fmt::format("{:%j}", d), fmt::format_error);
+  // A floating-point day count that doesn't fit in int.
+  auto fd = std::chrono::duration(1e300);
+  EXPECT_THROW((void)fmt::format("{:%j}", fd), fmt::format_error);
 }
 
 TEST(chrono_test, year_month_day) {
diff --git a/test/color-test.cc b/test/color-test.cc
index 45d8cd92..1ff5235a 100644
--- a/test/color-test.cc
+++ b/test/color-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - color tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -136,3 +136,15 @@ TEST(color_test, print) {
   EXPECT_WRITE(stdout, fmt::print(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"),
                "\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m");
 }
+
+TEST(color_test, println) {
+  EXPECT_WRITE(stdout,
+               fmt::println(fg(fmt::rgb(255, 20, 30)), "rgb(255,20,30)"),
+               "\x1b[38;2;255;020;030mrgb(255,20,30)\x1b[0m\n");
+  EXPECT_WRITE(stdout, fmt::println(fmt::emphasis::bold, "bold"),
+               "\x1b[1mbold\x1b[0m\n");
+  EXPECT_WRITE(
+      stdout,
+      fmt::println(fg(fmt::color::blue) | fmt::emphasis::bold, "blue/bold"),
+      "\x1b[1m\x1b[38;2;000;000;255mblue/bold\x1b[0m\n");
+}
diff --git a/test/compile-error-test/CMakeLists.txt b/test/compile-error-test/CMakeLists.txt
index 4725d333..5b57a2cf 100644
--- a/test/compile-error-test/CMakeLists.txt
+++ b/test/compile-error-test/CMakeLists.txt
@@ -239,6 +239,16 @@ if (CMAKE_CXX_STANDARD GREATER_EQUAL 20)
   #endif
     "
     ERROR)
+  expect_compile(
+    format-string-embedded-nul-error
+    "
+    #if FMT_USE_CONSTEVAL
+      fmt::format(\"a\\0{}\");
+    #else
+      #error
+    #endif
+    "
+    ERROR)
 
   # Compile-time argument name check
   expect_compile(
diff --git a/test/compile-test.cc b/test/compile-test.cc
index 9975b441..73e86f35 100644
--- a/test/compile-test.cc
+++ b/test/compile-test.cc
@@ -1,19 +1,22 @@
 // Formatting library for C++ - formatting library tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
 #include "fmt/compile.h"
 
+#include 
 #include 
 #include 
 #include 
 #include 
 
 #include "fmt/chrono.h"
+#include "fmt/color.h"
 #include "fmt/ranges.h"
+#include "fmt/std.h"
 #include "gmock/gmock.h"
 #include "gtest-extra.h"
 
@@ -87,7 +90,6 @@ TEST(compile_test, format_escape) {
   EXPECT_EQ("\"abc\"  ", fmt::format(FMT_COMPILE("{0:<7?}"), "abc"));
 }
 
-
 TEST(compile_test, format_specs) {
   EXPECT_EQ("42", fmt::format(FMT_COMPILE("{:x}"), 0x42));
   EXPECT_EQ("1.2 ms ",
@@ -224,6 +226,19 @@ TEST(compile_test, constexpr_formatted_size) {
   FMT_CONSTEXPR20 size_t str_size =
       fmt::formatted_size(FMT_COMPILE("{:s}"), "abc");
   EXPECT_EQ(str_size, 3);
+  FMT_CONSTEXPR20 size_t tuple_size = fmt::formatted_size(
+      FMT_COMPILE("{}"), fmt::join(std::tuple(1, 2, 3), ","));
+  EXPECT_EQ(tuple_size, 5);
+  FMT_CONSTEXPR20 size_t array_size = fmt::formatted_size(
+      FMT_COMPILE("{}"), fmt::join(std::array{1, 2, 3}, ","));
+  EXPECT_EQ(array_size, 5);
+  FMT_CONSTEXPR20 size_t styled_size = fmt::formatted_size(
+      FMT_COMPILE("{}"),
+      fmt::styled(std::array{1, 2, 3}, fmt::bg(fmt::color::green)));
+  EXPECT_EQ(styled_size, 32);
+  FMT_CONSTEXPR20 size_t variant_size = fmt::formatted_size(
+      FMT_COMPILE("{}"), std::variant{});
+  EXPECT_EQ(variant_size, 18);
 }
 
 TEST(compile_test, static_format) {
@@ -464,3 +479,13 @@ TEST(compile_test, constexpr_string_format) {
   EXPECT_TRUE(big);
 }
 #endif  // FMT_USE_CONSTEXPR_STRING
+
+namespace {
+struct compile_format_as_type {
+  int value;
+};
+int format_as(compile_format_as_type f) { return f.value; }
+} 
+TEST(compile_test, format_as) {
+  EXPECT_EQ("42", fmt::format(FMT_COMPILE("{}"), compile_format_as_type{42}));
+}
\ No newline at end of file
diff --git a/test/detect-stdfs.cc b/test/detect-stdfs.cc
index 2dc66538..ae3cdab5 100644
--- a/test/detect-stdfs.cc
+++ b/test/detect-stdfs.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - tests of formatters for standard library types
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/enforce-checks-test.cc b/test/enforce-checks-test.cc
index 960a7fcd..f513d828 100644
--- a/test/enforce-checks-test.cc
+++ b/test/enforce-checks-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - formatting library tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/format-impl-test.cc b/test/format-impl-test.cc
index 0d906c4e..eef041e4 100644
--- a/test/format-impl-test.cc
+++ b/test/format-impl-test.cc
@@ -1,11 +1,10 @@
 // Formatting library for C++ - formatting library implementation tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
-#include 
 #include 
 
 // clang-format off
@@ -14,7 +13,6 @@
 
 #include "fmt/format.h"
 #include "gmock/gmock.h"
-#include "util.h"
 
 using fmt::detail::bigint;
 using fmt::detail::fp;
diff --git a/test/format-test.cc b/test/format-test.cc
index c649aa4d..a1fccfc3 100644
--- a/test/format-test.cc
+++ b/test/format-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - formatting library tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -15,17 +15,17 @@
 
 #include   // uint32_t
 
-#include                // fegetexceptflag and FE_ALL_EXCEPT
-#include              // INT_MAX
-#include                // std::signbit
-#include   // std::condition_variable
-#include              // std::strlen
-#include             // std::back_inserter
-#include                 // std::list
-#include                // std::mutex
-#include               // std::string
-#include               // std::thread
-#include          // std::is_default_constructible
+#include         // fegetexceptflag and FE_ALL_EXCEPT
+#include       // INT_MAX
+#include         // std::signbit
+#include       // std::signal, SIGPIPE
+#include       // std::strlen
+#include      // std::back_inserter
+#include          // std::list
+#include         // std::mutex
+#include        // std::string
+#include        // std::thread
+#include   // std::is_default_constructible
 #if FMT_CPLUSPLUS > 201703L && FMT_HAS_INCLUDE()
 #  include 
 #endif
@@ -82,6 +82,11 @@ TEST(uint128_test, minus) {
   EXPECT_EQ(n - 2, 40);
 }
 
+TEST(uint128_test, bitwise_not) {
+  auto n = ~uint128(0x123456789abcdef0, 0x0fedcba987654321);
+  EXPECT_EQ(n, uint128(0xedcba9876543210f, 0xf0123456789abcde));
+}
+
 TEST(uint128_test, plus_assign) {
   auto n = uint128(32);
   n += uint128(10);
@@ -268,22 +273,6 @@ TEST(util_test, format_system_error) {
   fmt::format_system_error(message, EDOM, "test");
   auto ec = std::error_code(EDOM, std::generic_category());
   EXPECT_EQ(to_string(message), std::system_error(ec, "test").what());
-  message = fmt::memory_buffer();
-
-  // Check if std::allocator throws on allocating max size_t / 2 chars.
-  size_t max_size = max_value() / 2;
-  bool throws_on_alloc = false;
-  try {
-    auto alloc = std::allocator();
-    alloc.deallocate(alloc.allocate(max_size), max_size);
-  } catch (const std::bad_alloc&) {
-    throws_on_alloc = true;
-  }
-  if (!throws_on_alloc) {
-    fmt::print(stderr, "warning: std::allocator allocates {} chars\n",
-               max_size);
-    return;
-  }
 }
 
 TEST(util_test, system_error) {
@@ -1345,6 +1334,16 @@ TEST(format_test, format_int) {
                    "invalid format specifier");
   check_unknown_types(42, "bBdoxXnLc", "integer");
   EXPECT_EQ(fmt::format("{:c}", static_cast('x')), "x");
+  // The 'c' type treats all character types as unsigned for portability, so the
+  // representable range for char is [0, 255] and out-of-range values are
+  // reported as an error.
+  EXPECT_EQ(fmt::format("{:c}", 200), std::string(1, static_cast(200)));
+  EXPECT_EQ(fmt::format("{:c}", 255), std::string(1, static_cast(255)));
+  const char* msg = "character value out of range";
+  EXPECT_THROW_MSG((void)fmt::format("{:c}", -1), format_error, msg);
+  EXPECT_THROW_MSG((void)fmt::format("{:c}", -104), format_error, msg);
+  EXPECT_THROW_MSG((void)fmt::format("{:c}", 256), format_error, msg);
+  EXPECT_THROW_MSG((void)fmt::format("{:c}", 400u), format_error, msg);
 }
 
 TEST(format_test, format_bin) {
@@ -2609,6 +2608,24 @@ TEST(format_test, invalid_glibc_buffer) {
 
   fmt::print(file, "------\n");
 }
+
+TEST(format_test, print_to_broken_pipe) {
+  // Ignore SIGPIPE so that a failing write reports EPIPE instead of
+  // terminating the test process. It must stay ignored until the file is
+  // closed below because closing also flushes the remaining buffered data.
+  auto old_handler = std::signal(SIGPIPE, SIG_IGN);
+  {
+    auto pipe = fmt::pipe();
+    pipe.read_end.close();
+    auto write_end = pipe.write_end.fdopen("w");
+
+    // The data must exceed the file's buffer to force a flush during
+    // formatting, whose underlying write() then fails with EPIPE.
+    auto data = std::string(1024 * 1024, 'x');
+    EXPECT_THROW(fmt::print(write_end.get(), "{}", data), std::system_error);
+  }
+  std::signal(SIGPIPE, old_handler);
+}
 #endif  // FMT_USE_FCNTL
 
 // Only defined after the test case.
diff --git a/test/gtest-extra-test.cc b/test/gtest-extra-test.cc
index 5c860400..0f47dc98 100644
--- a/test/gtest-extra-test.cc
+++ b/test/gtest-extra-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - tests of custom Google Test assertions
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -9,7 +9,6 @@
 
 #include 
 
-#include 
 #include 
 #include 
 
diff --git a/test/gtest-extra.cc b/test/gtest-extra.cc
index 0f9fff5e..d819ec3c 100644
--- a/test/gtest-extra.cc
+++ b/test/gtest-extra.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - custom Google Test assertions
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/gtest-extra.h b/test/gtest-extra.h
index 71859835..7f5a9fe6 100644
--- a/test/gtest-extra.h
+++ b/test/gtest-extra.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - custom Google Test assertions
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/mock-allocator.h b/test/mock-allocator.h
index 0bf015a4..254849ee 100644
--- a/test/mock-allocator.h
+++ b/test/mock-allocator.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - mock allocator
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/module-test.cc b/test/module-test.cc
index bd9624f9..a7b67a4a 100644
--- a/test/module-test.cc
+++ b/test/module-test.cc
@@ -1,19 +1,9 @@
 // Formatting library for C++ - module tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
-//
-// Copyright (c) 2021 - present, Daniela Engert
-// All Rights Reserved
-// {fmt} module.
-
-#ifdef _MSC_FULL_VER
-// hide some implementation bugs in msvc
-// that are not essential to users of the module.
-#  define FMT_HIDE_MODULE_BUGS
-#endif
 
 #include 
 #include 
@@ -25,6 +15,7 @@
 #include 
 #include 
 #include 
+#include 
 
 #if (__has_include() || defined(__APPLE__) || \
      defined(__linux__)) &&                              \
@@ -42,14 +33,6 @@
 
 import fmt;
 
-// check for macros leaking from BMI
-static bool macro_leaked =
-#if defined(FMT_CORE_H_) || defined(FMT_FORMAT_H_)
-    true;
-#else
-    false;
-#endif
-
 #define FMT_OS_H_  // don't pull in os.h, neither directly nor indirectly
 #include "gtest-extra.h"
 
@@ -60,42 +43,11 @@ TEST(module_test, namespace) {
   ASSERT_TRUE(true);
 }
 
-namespace detail {
-bool oops_detail_namespace_is_visible;
-}
-
-namespace fmt {
-bool namespace_detail_invisible() {
-#if defined(FMT_HIDE_MODULE_BUGS) && defined(_MSC_FULL_VER) && \
-    _MSC_FULL_VER <= 193700000
-  // bug in msvc up to at least 17.7:
-
-  // the namespace is visible even when it is neither
-  // implicitly nor explicitly exported
-  return true;
-#else
-  using namespace detail;
-  // this fails to compile if fmt::detail is visible
-  return !oops_detail_namespace_is_visible;
-#endif
-}
-}  // namespace fmt
-
-// the non-exported namespace 'detail' must be invisible [module.interface]/2
-TEST(module_test, detail_namespace) {
-  EXPECT_TRUE(fmt::namespace_detail_invisible());
-}
-
-// macros must not be imported from a *named* module  [cpp.import]/5.1
+// Macros must not be imported from a named module [cpp.import]/5.1.
 TEST(module_test, macros) {
-#if defined(FMT_HIDE_MODULE_BUGS) && defined(_MSC_FULL_VER) && \
-    _MSC_FULL_VER <= 192930130
-  // bug in msvc up to 16.11-pre2:
-  // include-guard macros leak from BMI
-  // and even worse: they cannot be #undef-ined
-  macro_leaked = false;
+#if defined(FMT_BASE_H_) || defined(FMT_FORMAT_H_)
+  FAIL() << "Macros are leaking from a named module";
 #endif
-  EXPECT_FALSE(macro_leaked);
 }
 
 // The following is less about functional testing (that's done elsewhere)
@@ -167,78 +119,33 @@ TEST(module_test, format_to_n) {
 }
 
 TEST(module_test, format_args) {
-  auto no_args = fmt::format_args();
-  EXPECT_FALSE(no_args.get(1));
-
-  fmt::basic_format_args args = fmt::make_format_args(42);
-  EXPECT_TRUE(args.max_size() > 0);
-  auto arg0 = args.get(0);
-  EXPECT_TRUE(arg0);
-  decltype(arg0) arg_none;
-  EXPECT_FALSE(arg_none);
-  EXPECT_TRUE(arg0.type() != arg_none.type());
-}
-
-TEST(module_test, wformat_args) {
-  auto no_args = fmt::wformat_args();
-  EXPECT_FALSE(no_args.get(1));
-  fmt::basic_format_args args = fmt::make_wformat_args(42);
+  int n = 42;
+  auto store = fmt::make_format_args(n);
+  fmt::format_args args = store;
   EXPECT_TRUE(args.get(0));
-}
-
-TEST(module_test, dynamic_format_args) {
-  fmt::dynamic_format_arg_store dyn_store;
-  dyn_store.push_back(fmt::arg("a42", 42));
-  fmt::basic_format_args args = dyn_store;
-  EXPECT_FALSE(args.get(3));
-  EXPECT_TRUE(args.get(fmt::string_view("a42")));
-
-  fmt::dynamic_format_arg_store wdyn_store;
-  wdyn_store.push_back(fmt::arg(L"a42", 42));
-  fmt::basic_format_args wargs = wdyn_store;
-  EXPECT_FALSE(wargs.get(3));
-  EXPECT_TRUE(wargs.get(fmt::wstring_view(L"a42")));
+  EXPECT_FALSE(args.get(1));
 }
 
 TEST(module_test, vformat) {
-  EXPECT_EQ("42", fmt::vformat("{}", fmt::make_format_args(42)));
-  EXPECT_EQ(L"42",
-            fmt::vformat(fmt::wstring_view(L"{}"), fmt::make_wformat_args(42)));
+  int n = 42;
+  EXPECT_EQ("42", fmt::vformat("{}", fmt::make_format_args(n)));
 }
 
 TEST(module_test, vformat_to) {
-  auto store = fmt::make_format_args(42);
+  int n = 42;
+  auto store = fmt::make_format_args(n);
   std::string s;
   fmt::vformat_to(std::back_inserter(s), "{}", store);
   EXPECT_EQ("42", s);
-
-  char buffer[4] = {0};
-  fmt::vformat_to(buffer, "{:}", store);
-  EXPECT_EQ("42", std::string_view(buffer));
-
-  auto wstore = fmt::make_wformat_args(42);
-  std::wstring w;
-  fmt::vformat_to(std::back_inserter(w), L"{}", wstore);
-  EXPECT_EQ(L"42", w);
-
-  wchar_t wbuffer[4] = {0};
-  fmt::vformat_to(wbuffer, L"{:}", wstore);
-  EXPECT_EQ(L"42", std::wstring_view(wbuffer));
 }
 
 TEST(module_test, vformat_to_n) {
-  auto store = fmt::make_format_args(12345);
+  int n = 12345;
+  auto store = fmt::make_format_args(n);
   std::string s;
-  auto result = fmt::vformat_to_n(std::back_inserter(s), 1, "{}", store);
+  fmt::vformat_to_n(std::back_inserter(s), 1, "{}", store);
   char buffer[4] = {0};
   fmt::vformat_to_n(buffer, 3, "{:}", store);
-
-  auto wstore = fmt::make_wformat_args(12345);
-  std::wstring w;
-  auto wresult = fmt::vformat_to_n(std::back_inserter(w), 1,
-                                   fmt::wstring_view(L"{}"), wstore);
-  wchar_t wbuffer[4] = {0};
-  fmt::vformat_to_n(wbuffer, 3, fmt::wstring_view(L"{:}"), wstore);
 }
 
 std::string as_string(std::wstring_view text) {
@@ -249,18 +156,14 @@ std::string as_string(std::wstring_view text) {
 TEST(module_test, print) {
   EXPECT_WRITE(stdout, fmt::print("{}µ", 42), "42µ");
   EXPECT_WRITE(stderr, fmt::print(stderr, "{}µ", 4.2), "4.2µ");
-  EXPECT_WRITE(stdout, fmt::print(L"{}µ", 42), as_string(L"42µ"));
-  EXPECT_WRITE(stderr, fmt::print(stderr, L"{}µ", 4.2), as_string(L"4.2µ"));
 }
 
 TEST(module_test, vprint) {
-  EXPECT_WRITE(stdout, fmt::vprint("{:}µ", fmt::make_format_args(42)), "42µ");
-  EXPECT_WRITE(stderr, fmt::vprint(stderr, "{}", fmt::make_format_args(4.2)),
+  int n = 42;
+  double m = 4.2;
+  EXPECT_WRITE(stdout, fmt::vprint("{:}µ", fmt::make_format_args(n)), "42µ");
+  EXPECT_WRITE(stderr, fmt::vprint(stderr, "{}", fmt::make_format_args(m)),
                "4.2");
-  EXPECT_WRITE(stdout, fmt::vprint(L"{:}µ", fmt::make_wformat_args(42)),
-               as_string(L"42µ"));
-  EXPECT_WRITE(stderr, fmt::vprint(stderr, L"{}", fmt::make_wformat_args(42)),
-               as_string(L"42"));
 }
 
 TEST(module_test, named_args) {
@@ -275,7 +178,8 @@ TEST(module_test, literals) {
 }
 
 TEST(module_test, locale) {
-  auto store = fmt::make_format_args(4.2);
+  const double m = 4.2;
+  auto store = fmt::make_format_args(m);
   const auto classic = std::locale::classic();
   EXPECT_EQ("4.2", fmt::format(classic, "{:L}", 4.2));
   EXPECT_EQ("4.2", fmt::vformat(classic, "{:L}", store));
@@ -283,14 +187,6 @@ TEST(module_test, locale) {
   fmt::vformat_to(std::back_inserter(s), classic, "{:L}", store);
   EXPECT_EQ("4.2", s);
   EXPECT_EQ("4.2", fmt::format("{:L}", 4.2));
-
-  auto wstore = fmt::make_wformat_args(4.2);
-  EXPECT_EQ(L"4.2", fmt::format(classic, L"{:L}", 4.2));
-  EXPECT_EQ(L"4.2", fmt::vformat(classic, L"{:L}", wstore));
-  std::wstring w;
-  fmt::vformat_to(std::back_inserter(w), classic, L"{:L}", wstore);
-  EXPECT_EQ(L"4.2", w);
-  EXPECT_EQ(L"4.2", fmt::format(L"{:L}", 4.2));
 }
 
 TEST(module_test, string_view) {
@@ -313,34 +209,17 @@ TEST(module_test, memory_buffer) {
   EXPECT_EQ("42", to_string(buffer));
   nbuffer.clear();
   EXPECT_EQ(0u, to_string(nbuffer).size());
-
-  fmt::wmemory_buffer wbuffer;
-  EXPECT_EQ(0u, to_string(wbuffer).size());
-}
-
-TEST(module_test, is_char) {
-  EXPECT_TRUE(fmt::is_char());
-  EXPECT_TRUE(fmt::is_char());
-  EXPECT_TRUE(fmt::is_char());
-  EXPECT_TRUE(fmt::is_char());
-  EXPECT_TRUE(fmt::is_char());
-  EXPECT_FALSE(fmt::is_char());
 }
 
 TEST(module_test, ptr) {
   uintptr_t answer = 42;
   auto p = std::bit_cast(answer);
   EXPECT_EQ("0x2a", fmt::to_string(fmt::ptr(p)));
-  std::unique_ptr up(p);
-  EXPECT_EQ("0x2a", fmt::to_string(fmt::ptr(up)));
-  up.release();
-  auto sp = std::make_shared(0);
-  p = sp.get();
-  EXPECT_EQ(fmt::to_string(fmt::ptr(p)), fmt::to_string(fmt::ptr(sp)));
 }
 
 TEST(module_test, errors) {
-  auto store = fmt::make_format_args(42);
+  int n = 42;
+  auto store = fmt::make_format_args(n);
   EXPECT_THROW(throw fmt::format_error("oops"), std::exception);
   EXPECT_THROW(throw fmt::vsystem_error(0, "{}", store), std::system_error);
   EXPECT_THROW(throw fmt::system_error(0, "{}", 42), std::system_error);
@@ -365,8 +244,6 @@ TEST(module_test, error_code) {
             fmt::format("{0}", std::error_code(42, std::generic_category())));
   EXPECT_EQ("system:42",
             fmt::format("{0}", std::error_code(42, fmt::system_category())));
-  EXPECT_EQ(L"generic:42",
-            fmt::format(L"{0}", std::error_code(42, std::generic_category())));
 }
 
 TEST(module_test, format_int) {
@@ -385,23 +262,19 @@ TEST(module_test, formatter) { EXPECT_TRUE(test_formatter{}.check()); }
 TEST(module_test, join) {
   int arr[3] = {1, 2, 3};
   std::vector vec{1.0, 2.0, 3.0};
-  std::initializer_list il{1, 2, 3};
   auto sep = fmt::string_view(", ");
   EXPECT_EQ("1, 2, 3", to_string(fmt::join(arr + 0, arr + 3, sep)));
   EXPECT_EQ("1, 2, 3", to_string(fmt::join(arr, sep)));
   EXPECT_EQ("1, 2, 3", to_string(fmt::join(vec.begin(), vec.end(), sep)));
   EXPECT_EQ("1, 2, 3", to_string(fmt::join(vec, sep)));
-  EXPECT_EQ("1, 2, 3", to_string(fmt::join(il, sep)));
 
   auto wsep = fmt::wstring_view(L", ");
   EXPECT_EQ(L"1, 2, 3", fmt::format(L"{}", fmt::join(arr + 0, arr + 3, wsep)));
   EXPECT_EQ(L"1, 2, 3", fmt::format(L"{}", fmt::join(arr, wsep)));
-  EXPECT_EQ(L"1, 2, 3", fmt::format(L"{}", fmt::join(il, wsep)));
 }
 
 TEST(module_test, time) {
   auto time_now = std::time(nullptr);
-  EXPECT_TRUE(fmt::localtime(time_now).tm_year > 120);
   EXPECT_TRUE(fmt::gmtime(time_now).tm_year > 120);
   auto chrono_now = std::chrono::system_clock::now();
   EXPECT_TRUE(fmt::gmtime(chrono_now).tm_year > 120);
@@ -433,42 +306,17 @@ TEST(module_test, weekday) {
 TEST(module_test, printf) {
   EXPECT_WRITE(stdout, fmt::printf("%f", 42.123456), "42.123456");
   EXPECT_WRITE(stdout, fmt::printf("%d", 42), "42");
-  EXPECT_WRITE(stdout, fmt::printf(L"%f", 42.123456), as_string(L"42.123456"));
-  EXPECT_WRITE(stdout, fmt::printf(L"%d", 42), as_string(L"42"));
 }
 
 TEST(module_test, fprintf) {
   EXPECT_WRITE(stderr, fmt::fprintf(stderr, "%d", 42), "42");
-  EXPECT_WRITE(stderr, fmt::fprintf(stderr, L"%d", 42), as_string(L"42"));
 }
 
 TEST(module_test, sprintf) {
   EXPECT_EQ("42", fmt::sprintf("%d", 42));
-  EXPECT_EQ(L"42", fmt::sprintf(L"%d", 42));
-}
-
-TEST(module_test, vprintf) {
-  EXPECT_WRITE(stdout, fmt::vprintf("%d", fmt::make_printf_args(42)), "42");
-  EXPECT_WRITE(stdout, fmt::vprintf(L"%d", fmt::make_wprintf_args(42)),
-               as_string(L"42"));
-}
-
-TEST(module_test, vfprintf) {
-  auto args = fmt::make_printf_args(42);
-  EXPECT_WRITE(stderr, fmt::vfprintf(stderr, "%d", args), "42");
-  auto wargs = fmt::make_wprintf_args(42);
-  EXPECT_WRITE(stderr, fmt::vfprintf(stderr, L"%d", wargs), as_string(L"42"));
-}
-
-TEST(module_test, vsprintf) {
-  EXPECT_EQ("42", fmt::vsprintf("%d", fmt::make_printf_args(42)));
-  EXPECT_EQ(L"42", fmt::vsprintf(L"%d", fmt::make_wprintf_args(42)));
 }
 
 TEST(module_test, color) {
-  auto fg_check = fg(fmt::rgb(255, 200, 30));
-  auto bg_check = bg(fmt::color::dark_slate_gray) | fmt::emphasis::italic;
-  auto emphasis_check = fmt::emphasis::underline | fmt::emphasis::bold;
   EXPECT_EQ("\x1B[30m42\x1B[0m",
             fmt::format(fg(fmt::terminal_color::black), "{}", 42));
   EXPECT_EQ(L"\x1B[30m42\x1B[0m",
diff --git a/test/no-builtin-types-test.cc b/test/no-builtin-types-test.cc
index 1780220a..13f84151 100644
--- a/test/no-builtin-types-test.cc
+++ b/test/no-builtin-types-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - formatting library tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/noexception-test.cc b/test/noexception-test.cc
index 0e44e13b..2bf9605b 100644
--- a/test/noexception-test.cc
+++ b/test/noexception-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - Noexception tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/os-test.cc b/test/os-test.cc
index e789e87e..5bc9f7eb 100644
--- a/test/os-test.cc
+++ b/test/os-test.cc
@@ -1,13 +1,12 @@
 // Formatting library for C++ - tests of the OS-specific functionality
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
 #include "fmt/os.h"
 
-#include   // std::exit
 #include 
 #include 
 #include 
diff --git a/test/ostream-test.cc b/test/ostream-test.cc
index fc38c4c6..01fb4371 100644
--- a/test/ostream-test.cc
+++ b/test/ostream-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - std::ostream support tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/posix-mock-test.cc b/test/posix-mock-test.cc
index bcd330d4..15b173a0 100644
--- a/test/posix-mock-test.cc
+++ b/test/posix-mock-test.cc
@@ -1,6 +1,6 @@
 // Tests of the C++ interface to POSIX functions that require mocks
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -15,19 +15,19 @@
 #include 
 #include 
 
-#include 
 #include 
 
 #include "../src/os.cc"
 
 #ifdef _WIN32
 #  include 
+
+#  include   // UINT_MAX
 #  undef max
 #endif
 
 #include "gmock/gmock.h"
 #include "gtest-extra.h"
-#include "util.h"
 
 using fmt::buffered_file;
 
diff --git a/test/posix-mock.h b/test/posix-mock.h
index 54580871..0bcb9f52 100644
--- a/test/posix-mock.h
+++ b/test/posix-mock.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - mocks of POSIX functions
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/printf-test.cc b/test/printf-test.cc
index f232263c..baf2b168 100644
--- a/test/printf-test.cc
+++ b/test/printf-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - printf tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -9,7 +9,6 @@
 
 #include 
 #include 
-#include 
 
 #include "fmt/xchar.h"  // DEPRECATED!
 #include "gtest-extra.h"
@@ -47,6 +46,14 @@ auto test_sprintf(fmt::basic_string_view format, const Args&... args)
 
 TEST(printf_test, no_args) { EXPECT_EQ("test", test_sprintf("test")); }
 
+TEST(printf_test, trailing_percent) {
+  EXPECT_THROW_MSG(test_sprintf("%"), format_error, "invalid format string");
+  EXPECT_THROW_MSG(test_sprintf("hello%"), format_error,
+                   "invalid format string");
+  EXPECT_THROW_MSG(test_sprintf("%1$d%", 1, 2), format_error,
+                   "invalid format string");
+}
+
 TEST(printf_test, escape) {
   EXPECT_EQ("%", test_sprintf("%%"));
   EXPECT_EQ("before %", test_sprintf("before %%"));
@@ -77,8 +84,6 @@ TEST(printf_test, number_is_too_big_in_arg_index) {
 }
 
 TEST(printf_test, switch_arg_indexing) {
-  EXPECT_THROW_MSG(test_sprintf("%1$d%", 1, 2), format_error,
-                   "cannot switch from manual to automatic argument indexing");
   EXPECT_THROW_MSG(test_sprintf(format("%1$d%{}d", big_num), 1, 2),
                    format_error, "number is too big");
   EXPECT_THROW_MSG(test_sprintf("%1$d%d", 1, 2), format_error,
@@ -111,6 +116,17 @@ TEST(printf_test, invalid_arg_index) {
                    "argument not found");
 }
 
+TEST(printf_test, zero_positional_width_precision) {
+  // A '0' positional index for a '*' width or precision must be rejected. Use
+  // enough arguments to exercise the unpacked argument storage path.
+  EXPECT_THROW_MSG(test_sprintf("%*0$d", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
+                                13, 14, 15, 16),
+                   format_error, "argument not found");
+  EXPECT_THROW_MSG(test_sprintf("%.*0$d", 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12,
+                                13, 14, 15, 16),
+                   format_error, "argument not found");
+}
+
 TEST(printf_test, default_align_right) {
   EXPECT_PRINTF("   42", "%5d", 42);
   EXPECT_PRINTF("  abc", "%5s", "abc");
@@ -313,7 +329,8 @@ TEST(printf_test, positional_precision) {
   EXPECT_EQ("Hell", test_sprintf("%2$.*1$s", 4, "Hello"));
   EXPECT_THROW_MSG(test_sprintf("%2$.*1$d", 5.0, 42), format_error,
                    "precision is not integer");
-  EXPECT_THROW_MSG(test_sprintf("%2$.*1$d"), format_error, "argument not found");
+  EXPECT_THROW_MSG(test_sprintf("%2$.*1$d"), format_error,
+                   "argument not found");
   EXPECT_THROW_MSG(test_sprintf("%2$.*1$d", big_num, 42), format_error,
                    "number is too big");
 }
@@ -322,7 +339,8 @@ TEST(printf_test, positional_width_and_precision) {
   EXPECT_EQ("  00042", test_sprintf("%3$*1$.*2$d", 7, 5, 42));
   EXPECT_EQ("     ab", test_sprintf("%3$*1$.*2$s", 7, 2, "abcdef"));
   EXPECT_EQ("  00042", test_sprintf("%3$*1$.*2$x", 7, 5, 0x42));
-  EXPECT_EQ("100.4400000", test_sprintf("%6$-*5$.*4$f%3$s%2$s%1$s", "", "", "", 7, 4, 100.44));
+  EXPECT_EQ("100.4400000",
+            test_sprintf("%6$-*5$.*4$f%3$s%2$s%1$s", "", "", "", 7, 4, 100.44));
 }
 
 template  struct make_signed {
@@ -555,3 +573,8 @@ TEST(printf_test, make_printf_args) {
             fmt::vsprintf(fmt::basic_string_view(L"[%d] %s happened"),
                           {fmt::make_printf_args(n, L"something")}));
 }
+
+TEST(printf_test, trailing_percent_non_nul_terminated) {
+  auto p = std::unique_ptr(new char('%'));
+  EXPECT_THROW(fmt::sprintf(fmt::string_view(p.get(), 1)), format_error);
+}
diff --git a/test/ranges-odr-test.cc b/test/ranges-odr-test.cc
index 6115dcb7..9be56243 100644
--- a/test/ranges-odr-test.cc
+++ b/test/ranges-odr-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - the core API
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/ranges-test.cc b/test/ranges-test.cc
index 7ec0d76c..4afd6c72 100644
--- a/test/ranges-test.cc
+++ b/test/ranges-test.cc
@@ -10,7 +10,6 @@
 #include 
 #include 
 #include 
-#include 
 #include 
 #include 
 #include 
@@ -272,6 +271,23 @@ TEST(ranges_test, disabled_range_formatting_of_path) {
             fmt::range_format::disabled);
 }
 
+template  struct optional_like {
+  auto begin() const -> const T*;
+  auto end() const -> const T*;
+
+  bool has_value() const noexcept;
+  T& value() &;
+  const T& value() const&;
+  T&& value() &&;
+  const T&& value() const&&;
+};
+
+TEST(ranges_test, disabled_range_formatting_of_optional) {
+  // (C++26) Has a range support for std::optional.
+  EXPECT_EQ((fmt::range_format_kind, char>::value),
+            fmt::range_format::disabled);
+}
+
 struct vector_string : std::vector {
   using base = std::vector;
   using base::base;
@@ -710,10 +726,17 @@ struct codec_mask {
   int except = 0;
 };
 
+// A named functor instead of a lambda to avoid -Wsubobject-linkage: a lambda
+// gives the filter_view type internal linkage, which propagates to the
+// formatter base class via format_as.
+struct not_equal {
+  int value;
+  bool operator()(int c) const { return c != value; }
+};
+
 auto format_as(codec_mask mask) {
-  // Careful not to capture param by reference here, it will dangle.
   return codec_mask::codecs |
-         std::views::filter([mask](auto c) { return c != mask.except; });
+         std::views::filter(not_equal{mask.except});
 }
 }  // namespace views_filter_view_test
 
diff --git a/test/scan-test.cc b/test/scan-test.cc
index d4f9e413..2c6f29e0 100644
--- a/test/scan-test.cc
+++ b/test/scan-test.cc
@@ -1,14 +1,12 @@
 // Formatting library for C++ - scanning API test
 //
-// Copyright (c) 2019 - present, Victor Zverovich
+// Copyright (c) 2019 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
 #include "scan.h"
 
-#include 
-
 #include 
 #include 
 
diff --git a/test/scan.h b/test/scan.h
index 257ac8d7..951410f1 100644
--- a/test/scan.h
+++ b/test/scan.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - scanning API proof of concept
 //
-// Copyright (c) 2019 - present, Victor Zverovich
+// Copyright (c) 2019 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/std-test.cc b/test/std-test.cc
index 1684a5ed..6fc92938 100644
--- a/test/std-test.cc
+++ b/test/std-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - tests of formatters for standard library types
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
@@ -402,6 +402,67 @@ TEST(std_test, exception) {
                 StartsWith("std::filesystem::filesystem_error: "));
   }
 #endif
+
+#if FMT_USE_RTTI
+  // Nested exceptions (e.g. from std::throw_with_nested) are unwound.
+  try {
+    try {
+      throw std::runtime_error("inner");
+    } catch (...) {
+      std::throw_with_nested(std::runtime_error("outer"));
+    }
+  } catch (const std::exception& ex) {
+    EXPECT_EQ("outer: inner", fmt::format("{}", ex));
+  }
+
+  // Multiple levels of nesting.
+  try {
+    try {
+      try {
+        throw std::runtime_error("level 3");
+      } catch (...) {
+        std::throw_with_nested(std::runtime_error("level 2"));
+      }
+    } catch (...) {
+      std::throw_with_nested(std::runtime_error("level 1"));
+    }
+  } catch (const std::exception& ex) {
+    EXPECT_EQ("level 1: level 2: level 3", fmt::format("{}", ex));
+  }
+#endif  // FMT_USE_RTTI
+}
+
+TEST(std_test, exception_ptr) {
+  std::exception_ptr p1 = nullptr;
+  std::exception_ptr p2;
+
+  try {
+    using namespace my_ns1::my_ns2;
+    throw my_exception("My Exception");
+  } catch (...) {
+    p2 = std::current_exception();
+  }
+
+  EXPECT_EQ(fmt::format("{}", p1), "none");
+  EXPECT_EQ(fmt::format("{}", p2), "My Exception");
+
+#if FMT_USE_RTTI
+  EXPECT_EQ(fmt::format("{:t}", p2),
+            "my_ns1::my_ns2::my_exception: My Exception");
+
+  // Nested exceptions are unwound through an exception_ptr too.
+  std::exception_ptr p3;
+  try {
+    try {
+      throw std::runtime_error("inner");
+    } catch (...) {
+      std::throw_with_nested(std::runtime_error("outer"));
+    }
+  } catch (...) {
+    p3 = std::current_exception();
+  }
+  EXPECT_EQ(fmt::format("{}", p3), "outer: inner");
+#endif  // FMT_USE_RTTI
 }
 
 #if FMT_USE_RTTI
@@ -409,7 +470,7 @@ TEST(std_test, type_info) {
   EXPECT_EQ(fmt::format("{}", typeid(std::runtime_error)),
             "std::runtime_error");
 }
-#endif
+#endif  // FMT_USE_RTTI
 
 #if FMT_USE_BITINT
 FMT_PRAGMA_CLANG(diagnostic ignored "-Wbit-int-extension")
diff --git a/test/test-assert.h b/test/test-assert.h
index bec3dfc3..787780b1 100644
--- a/test/test-assert.h
+++ b/test/test-assert.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - test version of FMT_ASSERT
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/test-main.cc b/test/test-main.cc
index 39d2789d..496fcd8b 100644
--- a/test/test-main.cc
+++ b/test/test-main.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - test main function.
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/unicode-test.cc b/test/unicode-test.cc
index 2b59cff9..58c5d03e 100644
--- a/test/unicode-test.cc
+++ b/test/unicode-test.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - Unicode tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/util.cc b/test/util.cc
index 8ea26610..1e2fa77d 100644
--- a/test/util.cc
+++ b/test/util.cc
@@ -1,6 +1,6 @@
 // Formatting library for C++ - test utilities
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/util.h b/test/util.h
index 1f2916a9..b02ceab1 100644
--- a/test/util.h
+++ b/test/util.h
@@ -1,6 +1,6 @@
 // Formatting library for C++ - test utilities
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
diff --git a/test/xchar-test.cc b/test/xchar-test.cc
index cfe8389d..23be4bed 100644
--- a/test/xchar-test.cc
+++ b/test/xchar-test.cc
@@ -1,15 +1,13 @@
 // Formatting library for C++ - formatting library tests
 //
-// Copyright (c) 2012 - present, Victor Zverovich
+// Copyright (c) 2012 - present, Victor Zverovich and {fmt} contributors
 // All rights reserved.
 //
 // For the license information refer to format.h.
 
 #include "fmt/xchar.h"
 
-#include 
 #include 
-#include 
 #include 
 
 #include "fmt/chrono.h"
@@ -146,6 +144,26 @@ TEST(format_test, wide_format_to_n) {
   EXPECT_EQ(L"BC x", fmt::wstring_view(buffer, 4));
 }
 
+TEST(format_test, wide_format_to_n_runtime) {
+  wchar_t buffer[4];
+  buffer[3] = L'x';
+  auto result = fmt::format_to_n(buffer, 3, fmt::runtime(L"{}"), 12345);
+  EXPECT_EQ(5u, result.size);
+  EXPECT_EQ(buffer + 3, result.out);
+  EXPECT_EQ(L"123x", fmt::wstring_view(buffer, 4));
+  buffer[0] = L'x';
+  buffer[1] = L'x';
+  buffer[2] = L'x';
+  result = fmt::format_to_n(buffer, 3, fmt::runtime(L"{}"), L'A');
+  EXPECT_EQ(1u, result.size);
+  EXPECT_EQ(buffer + 1, result.out);
+  EXPECT_EQ(L"Axxx", fmt::wstring_view(buffer, 4));
+  result = fmt::format_to_n(buffer, 3, fmt::runtime(L"{}{} "), L'B', L'C');
+  EXPECT_EQ(3u, result.size);
+  EXPECT_EQ(buffer + 3, result.out);
+  EXPECT_EQ(L"BC x", fmt::wstring_view(buffer, 4));
+}
+
 TEST(xchar_test, named_arg_udl) {
   using namespace fmt::literals;
   auto udl_a =