42 changed files with 11211 additions and 5322 deletions
-
57.clang-format
-
2pagelayout_editor/tools/pl_edit_tool.cpp
-
2pagelayout_editor/tools/pl_point_editor.cpp
-
208thirdparty/fmt/CMakeLists.txt
-
2628thirdparty/fmt/ChangeLog.md
-
27thirdparty/fmt/LICENSE
-
484thirdparty/fmt/README.md
-
126thirdparty/fmt/include/fmt/args.h
-
3077thirdparty/fmt/include/fmt/base.h
-
732thirdparty/fmt/include/fmt/chrono.h
-
242thirdparty/fmt/include/fmt/color.h
-
115thirdparty/fmt/include/fmt/compile.h
-
2925thirdparty/fmt/include/fmt/core.h
-
446thirdparty/fmt/include/fmt/format-inl.h
-
1697thirdparty/fmt/include/fmt/format.h
-
222thirdparty/fmt/include/fmt/os.h
-
114thirdparty/fmt/include/fmt/ostream.h
-
265thirdparty/fmt/include/fmt/printf.h
-
507thirdparty/fmt/include/fmt/ranges.h
-
520thirdparty/fmt/include/fmt/std.h
-
166thirdparty/fmt/include/fmt/xchar.h
-
95thirdparty/fmt/src/fmt.cc
-
141thirdparty/fmt/src/os.cc
-
15thirdparty/fmt/support/Android.mk
-
1thirdparty/fmt/support/AndroidManifest.xml
-
4thirdparty/fmt/support/README
-
19thirdparty/fmt/support/Vagrantfile
-
1thirdparty/fmt/support/bazel/.bazelversion
-
20thirdparty/fmt/support/bazel/BUILD.bazel
-
6thirdparty/fmt/support/bazel/MODULE.bazel
-
28thirdparty/fmt/support/bazel/README.md
-
2thirdparty/fmt/support/bazel/WORKSPACE.bazel
-
132thirdparty/fmt/support/build.gradle
-
43thirdparty/fmt/support/check-commits
-
54thirdparty/fmt/support/cmake/cxx14.cmake
-
581thirdparty/fmt/support/docopt.py
-
218thirdparty/fmt/support/manage.py
-
44thirdparty/fmt/support/mkdocs
-
48thirdparty/fmt/support/mkdocs.yml
-
201thirdparty/fmt/support/printable.py
-
317thirdparty/fmt/support/python/mkdocstrings_handlers/cxx/__init__.py
-
1thirdparty/fmt/support/python/mkdocstrings_handlers/cxx/templates/README
@ -0,0 +1,57 @@ |
|||
# minimum clang-format 10 |
|||
BasedOnStyle: LLVM |
|||
AccessModifierOffset: -4 |
|||
AlignAfterOpenBracket: Align |
|||
AlignConsecutiveAssignments: false |
|||
AlignConsecutiveDeclarations: true |
|||
AlignOperands: true |
|||
AlignTrailingComments: true |
|||
AllowAllConstructorInitializersOnNextLine: false |
|||
AllowAllParametersOfDeclarationOnNextLine: false |
|||
AllowShortBlocksOnASingleLine: Never |
|||
AllowShortCaseLabelsOnASingleLine: true |
|||
AllowShortFunctionsOnASingleLine: InlineOnly |
|||
AllowShortIfStatementsOnASingleLine: Never |
|||
AllowShortLambdasOnASingleLine: None |
|||
AllowShortLoopsOnASingleLine: false |
|||
AlwaysBreakAfterReturnType: None |
|||
AlwaysBreakBeforeMultilineStrings: false |
|||
AlwaysBreakTemplateDeclarations: Yes |
|||
BinPackArguments: true |
|||
BinPackParameters: true |
|||
BreakBeforeBinaryOperators: NonAssignment |
|||
BreakBeforeBraces: Allman |
|||
BreakBeforeTernaryOperators: true |
|||
BreakConstructorInitializers: AfterColon |
|||
BreakConstructorInitializersBeforeComma: false |
|||
BreakStringLiterals: true |
|||
ColumnLimit: 100 |
|||
ConstructorInitializerAllOnOneLineOrOnePerLine: false |
|||
ConstructorInitializerIndentWidth: 8 |
|||
ContinuationIndentWidth: 8 |
|||
Cpp11BracedListStyle: false |
|||
DerivePointerAlignment: false |
|||
DisableFormat: false |
|||
ForEachMacros: [ BOOST_FOREACH ] |
|||
IndentCaseLabels: false |
|||
IndentWidth: 4 |
|||
IndentWrappedFunctionNames: false |
|||
KeepEmptyLinesAtTheStartOfBlocks: false |
|||
Language: Cpp |
|||
MaxEmptyLinesToKeep: 2 |
|||
NamespaceIndentation: Inner |
|||
PointerAlignment: Left |
|||
ReflowComments: false |
|||
SortIncludes: false |
|||
SpaceAfterCStyleCast: true |
|||
SpaceBeforeAssignmentOperators: true |
|||
SpaceBeforeParens: Never |
|||
SpaceInEmptyParentheses: false |
|||
SpacesBeforeTrailingComments: 1 |
|||
SpacesInAngles: false |
|||
SpacesInCStyleCastParentheses: false |
|||
SpacesInParentheses: true |
|||
SpacesInSquareBrackets: false |
|||
Standard: c++11 |
|||
TabWidth: 4 |
|||
UseTab: Never |
2628
thirdparty/fmt/ChangeLog.md
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,27 @@ |
|||
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 |
|||
"Software"), to deal in the Software without restriction, including |
|||
without limitation the rights to use, copy, modify, merge, publish, |
|||
distribute, sublicense, and/or sell copies of the Software, and to |
|||
permit persons to whom the Software is furnished to do so, subject to |
|||
the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be |
|||
included in all copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, |
|||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF |
|||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND |
|||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE |
|||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION |
|||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION |
|||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
|||
|
|||
--- Optional exception to the license --- |
|||
|
|||
As an exception, if, as a result of your compiling your source code, portions |
|||
of this Software are embedded into a machine-executable object form of such |
|||
source code, you may redistribute such embedded portions in such object form |
|||
without including the above copyright and permission notices. |
@ -0,0 +1,484 @@ |
|||
<img src="https://user-images.githubusercontent.com/576385/156254208-f5b743a9-88cf-439d-b0c0-923d53e8d551.png" alt="{fmt}" width="25%"/> |
|||
|
|||
[](https://github.com/fmtlib/fmt/actions?query=workflow%3Alinux) |
|||
[](https://github.com/fmtlib/fmt/actions?query=workflow%3Amacos) |
|||
[](https://github.com/fmtlib/fmt/actions?query=workflow%3Awindows) |
|||
[](https://bugs.chromium.org/p/oss-fuzz/issues/list?\%0Acolspec=ID%20Type%20Component%20Status%20Proj%20Reported%20Owner%20\%0ASummary&q=proj%3Dfmt&can=1) |
|||
[](https://stackoverflow.com/questions/tagged/fmt) |
|||
[](https://securityscorecards.dev/viewer/?uri=github.com/fmtlib/fmt) |
|||
|
|||
**{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: <https://www.stopputin.net/>. |
|||
|
|||
[Documentation](https://fmt.dev) |
|||
|
|||
[Cheat Sheets](https://hackingcpp.com/cpp/libs/fmt.html) |
|||
|
|||
Q&A: ask questions on [StackOverflow with the tag |
|||
fmt](https://stackoverflow.com/questions/tagged/fmt). |
|||
|
|||
Try {fmt} in [Compiler Explorer](https://godbolt.org/z/8Mx1EW73v). |
|||
|
|||
# Features |
|||
|
|||
- Simple [format API](https://fmt.dev/latest/api/) with positional |
|||
arguments for localization |
|||
- Implementation of [C++20 |
|||
std::format](https://en.cppreference.com/w/cpp/utility/format) and |
|||
[C++23 std::print](https://en.cppreference.com/w/cpp/io/print) |
|||
- [Format string syntax](https://fmt.dev/latest/syntax/) similar |
|||
to Python\'s |
|||
[format](https://docs.python.org/3/library/stdtypes.html#str.format) |
|||
- Fast IEEE 754 floating-point formatter with correct rounding, |
|||
shortness and round-trip guarantees using the |
|||
[Dragonbox](https://github.com/jk-jeon/dragonbox) algorithm |
|||
- Portable Unicode support |
|||
- Safe [printf |
|||
implementation](https://fmt.dev/latest/api/#printf-formatting) |
|||
including the POSIX extension for positional arguments |
|||
- Extensibility: [support for user-defined |
|||
types](https://fmt.dev/latest/api/#formatting-user-defined-types) |
|||
- High performance: faster than common standard library |
|||
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) |
|||
- Small code size both in terms of source code with the minimum |
|||
configuration consisting of just three files, `core.h`, `format.h` |
|||
and `format-inl.h`, and compiled code; see [Compile time and code |
|||
bloat](#compile-time-and-code-bloat) |
|||
- Reliability: the library has an extensive set of |
|||
[tests](https://github.com/fmtlib/fmt/tree/master/test) and is |
|||
[continuously fuzzed](https://bugs.chromium.org/p/oss-fuzz/issues/list?colspec=ID%20Type%20Component%20Status%20Proj%20Reported%20Owner%20Summary&q=proj%3Dfmt&can=1) |
|||
- Safety: the library is fully type-safe, errors in format strings can |
|||
be reported at compile time, automatic memory management prevents |
|||
buffer overflow errors |
|||
- Ease of use: small self-contained code base, no external |
|||
dependencies, permissive MIT |
|||
[license](https://github.com/fmtlib/fmt/blob/master/LICENSE) |
|||
- [Portability](https://fmt.dev/latest/#portability) with |
|||
consistent output across platforms and support for older compilers |
|||
- Clean warning-free codebase even on high warning levels such as |
|||
`-Wall -Wextra -pedantic` |
|||
- Locale independence by default |
|||
- Optional header-only configuration enabled with the |
|||
`FMT_HEADER_ONLY` macro |
|||
|
|||
See the [documentation](https://fmt.dev) for more details. |
|||
|
|||
# Examples |
|||
|
|||
**Print to stdout** ([run](https://godbolt.org/z/Tevcjh)) |
|||
|
|||
``` c++ |
|||
#include <fmt/core.h> |
|||
|
|||
int main() { |
|||
fmt::print("Hello, world!\n"); |
|||
} |
|||
``` |
|||
|
|||
**Format a string** ([run](https://godbolt.org/z/oK8h33)) |
|||
|
|||
``` c++ |
|||
std::string s = fmt::format("The answer is {}.", 42); |
|||
// s == "The answer is 42." |
|||
``` |
|||
|
|||
**Format a string using positional arguments** |
|||
([run](https://godbolt.org/z/Yn7Txe)) |
|||
|
|||
``` c++ |
|||
std::string s = fmt::format("I'd rather be {1} than {0}.", "right", "happy"); |
|||
// s == "I'd rather be happy than right." |
|||
``` |
|||
|
|||
**Print dates and times** ([run](https://godbolt.org/z/c31ExdY3W)) |
|||
|
|||
``` c++ |
|||
#include <fmt/chrono.h> |
|||
|
|||
int main() { |
|||
auto now = std::chrono::system_clock::now(); |
|||
fmt::print("Date and time: {}\n", now); |
|||
fmt::print("Time: {:%H:%M}\n", now); |
|||
} |
|||
``` |
|||
|
|||
Output: |
|||
|
|||
Date and time: 2023-12-26 19:10:31.557195597 |
|||
Time: 19:10 |
|||
|
|||
**Print a container** ([run](https://godbolt.org/z/MxM1YqjE7)) |
|||
|
|||
``` c++ |
|||
#include <vector> |
|||
#include <fmt/ranges.h> |
|||
|
|||
int main() { |
|||
std::vector<int> v = {1, 2, 3}; |
|||
fmt::print("{}\n", v); |
|||
} |
|||
``` |
|||
|
|||
Output: |
|||
|
|||
[1, 2, 3] |
|||
|
|||
**Check a format string at compile time** |
|||
|
|||
``` c++ |
|||
std::string s = fmt::format("{:d}", "I am not a number"); |
|||
``` |
|||
|
|||
This gives a compile-time error in C++20 because `d` is an invalid |
|||
format specifier for a string. |
|||
|
|||
**Write a file from a single thread** |
|||
|
|||
``` c++ |
|||
#include <fmt/os.h> |
|||
|
|||
int main() { |
|||
auto out = fmt::output_file("guide.txt"); |
|||
out.print("Don't {}", "Panic"); |
|||
} |
|||
``` |
|||
|
|||
This can be [5 to 9 times faster than |
|||
fprintf](http://www.zverovich.net/2020/08/04/optimal-file-buffer-size.html). |
|||
|
|||
**Print with colors and text styles** |
|||
|
|||
``` c++ |
|||
#include <fmt/color.h> |
|||
|
|||
int main() { |
|||
fmt::print(fg(fmt::color::crimson) | fmt::emphasis::bold, |
|||
"Hello, {}!\n", "world"); |
|||
fmt::print(fg(fmt::color::floral_white) | bg(fmt::color::slate_gray) | |
|||
fmt::emphasis::underline, "Olá, {}!\n", "Mundo"); |
|||
fmt::print(fg(fmt::color::steel_blue) | fmt::emphasis::italic, |
|||
"你好{}!\n", "世界"); |
|||
} |
|||
``` |
|||
|
|||
Output on a modern terminal with Unicode support: |
|||
|
|||
 |
|||
|
|||
# Benchmarks |
|||
|
|||
## Speed tests |
|||
|
|||
| Library | Method | Run Time, s | |
|||
|-------------------|---------------|-------------| |
|||
| libc | printf | 0.91 | |
|||
| libc++ | std::ostream | 2.49 | |
|||
| {fmt} 9.1 | fmt::print | 0.74 | |
|||
| Boost Format 1.80 | boost::format | 6.26 | |
|||
| Folly Format | folly::format | 1.87 | |
|||
|
|||
{fmt} is the fastest of the benchmarked methods, \~20% faster than |
|||
`printf`. |
|||
|
|||
The above results were generated by building `tinyformat_test.cpp` on |
|||
macOS 12.6.1 with `clang++ -O3 -DNDEBUG -DSPEED_TEST -DHAVE_FORMAT`, and |
|||
taking the best of three runs. In the test, the format string |
|||
`"%0.10f:%04d:%+g:%s:%p:%c:%%\n"` or equivalent is filled 2,000,000 |
|||
times with output sent to `/dev/null`; for further details refer to the |
|||
[source](https://github.com/fmtlib/format-benchmark/blob/master/src/tinyformat-test.cc). |
|||
|
|||
{fmt} is up to 20-30x faster than `std::ostringstream` and `sprintf` on |
|||
IEEE754 `float` and `double` formatting |
|||
([dtoa-benchmark](https://github.com/fmtlib/dtoa-benchmark)) and faster |
|||
than [double-conversion](https://github.com/google/double-conversion) |
|||
and [ryu](https://github.com/ulfjack/ryu): |
|||
|
|||
[](https://fmt.dev/unknown_mac64_clang12.0.html) |
|||
|
|||
## Compile time and code bloat |
|||
|
|||
The script [bloat-test.py][test] from [format-benchmark][bench] tests compile |
|||
time and code bloat for nontrivial projects. It generates 100 translation units |
|||
and uses `printf()` or its alternative five times in each to simulate a |
|||
medium-sized project. The resulting executable size and compile time (Apple |
|||
clang version 15.0.0 (clang-1500.1.0.2.5), macOS Sonoma, best of three) is shown |
|||
in the following tables. |
|||
|
|||
[test]: https://github.com/fmtlib/format-benchmark/blob/master/bloat-test.py |
|||
[bench]: https://github.com/fmtlib/format-benchmark |
|||
|
|||
**Optimized build (-O3)** |
|||
|
|||
| Method | Compile Time, s | Executable size, KiB | Stripped size, KiB | |
|||
|---------------|-----------------|----------------------|--------------------| |
|||
| printf | 1.6 | 54 | 50 | |
|||
| IOStreams | 25.9 | 98 | 84 | |
|||
| fmt 83652df | 4.8 | 54 | 50 | |
|||
| tinyformat | 29.1 | 161 | 136 | |
|||
| Boost Format | 55.0 | 530 | 317 | |
|||
|
|||
{fmt} is fast to compile and is comparable to `printf` in terms of per-call |
|||
binary size (within a rounding error on this system). |
|||
|
|||
**Non-optimized build** |
|||
|
|||
| Method | Compile Time, s | Executable size, KiB | Stripped size, KiB | |
|||
|---------------|-----------------|----------------------|--------------------| |
|||
| printf | 1.4 | 54 | 50 | |
|||
| IOStreams | 23.4 | 92 | 68 | |
|||
| {fmt} 83652df | 4.4 | 89 | 85 | |
|||
| tinyformat | 24.5 | 204 | 161 | |
|||
| Boost Format | 36.4 | 831 | 462 | |
|||
|
|||
`libc`, `lib(std)c++`, and `libfmt` are all linked as shared libraries |
|||
to compare formatting function overhead only. Boost Format is a |
|||
header-only library so it doesn\'t provide any linkage options. |
|||
|
|||
## Running the tests |
|||
|
|||
Please refer to [Building the |
|||
library](https://fmt.dev/latest/get-started/#building-from-source) for |
|||
instructions on how to build the library and run the unit tests. |
|||
|
|||
Benchmarks reside in a separate repository, |
|||
[format-benchmarks](https://github.com/fmtlib/format-benchmark), so to |
|||
run the benchmarks you first need to clone this repository and generate |
|||
Makefiles with CMake: |
|||
|
|||
$ git clone --recursive https://github.com/fmtlib/format-benchmark.git |
|||
$ cd format-benchmark |
|||
$ cmake . |
|||
|
|||
Then you can run the speed test: |
|||
|
|||
$ make speed-test |
|||
|
|||
or the bloat test: |
|||
|
|||
$ make bloat-test |
|||
|
|||
# Migrating code |
|||
|
|||
[clang-tidy](https://clang.llvm.org/extra/clang-tidy/) v18 provides the |
|||
[modernize-use-std-print](https://clang.llvm.org/extra/clang-tidy/checks/modernize/use-std-print.html) |
|||
check that is capable of converting occurrences of `printf` and |
|||
`fprintf` to `fmt::print` if configured to do so. (By default it |
|||
converts to `std::print`.) |
|||
|
|||
# Notable projects using this library |
|||
|
|||
- [0 A.D.](https://play0ad.com/): a free, open-source, cross-platform |
|||
real-time strategy game |
|||
- [AMPL/MP](https://github.com/ampl/mp): an open-source library for |
|||
mathematical programming |
|||
- [Apple's FoundationDB](https://github.com/apple/foundationdb): an open-source, |
|||
distributed, transactional key-value store |
|||
- [Aseprite](https://github.com/aseprite/aseprite): animated sprite |
|||
editor & pixel art tool |
|||
- [AvioBook](https://www.aviobook.aero/en): a comprehensive aircraft |
|||
operations suite |
|||
- [Blizzard Battle.net](https://battle.net/): an online gaming |
|||
platform |
|||
- [Celestia](https://celestia.space/): real-time 3D visualization of |
|||
space |
|||
- [Ceph](https://ceph.com/): a scalable distributed storage system |
|||
- [ccache](https://ccache.dev/): a compiler cache |
|||
- [ClickHouse](https://github.com/ClickHouse/ClickHouse): an |
|||
analytical database management system |
|||
- [Contour](https://github.com/contour-terminal/contour/): a modern |
|||
terminal emulator |
|||
- [CUAUV](https://cuauv.org/): Cornell University\'s autonomous |
|||
underwater vehicle |
|||
- [Drake](https://drake.mit.edu/): a planning, control, and analysis |
|||
toolbox for nonlinear dynamical systems (MIT) |
|||
- [Envoy](https://github.com/envoyproxy/envoy): C++ L7 proxy and |
|||
communication bus (Lyft) |
|||
- [FiveM](https://fivem.net/): a modification framework for GTA V |
|||
- [fmtlog](https://github.com/MengRao/fmtlog): a performant |
|||
fmtlib-style logging library with latency in nanoseconds |
|||
- [Folly](https://github.com/facebook/folly): Facebook open-source |
|||
library |
|||
- [GemRB](https://gemrb.org/): a portable open-source implementation |
|||
of Bioware's Infinity Engine |
|||
- [Grand Mountain |
|||
Adventure](https://store.steampowered.com/app/1247360/Grand_Mountain_Adventure/): |
|||
a beautiful open-world ski & snowboarding game |
|||
- [HarpyWar/pvpgn](https://github.com/pvpgn/pvpgn-server): Player vs |
|||
Player Gaming Network with tweaks |
|||
- [KBEngine](https://github.com/kbengine/kbengine): an open-source |
|||
MMOG server engine |
|||
- [Keypirinha](https://keypirinha.com/): a semantic launcher for |
|||
Windows |
|||
- [Kodi](https://kodi.tv/) (formerly xbmc): home theater software |
|||
- [Knuth](https://kth.cash/): high-performance Bitcoin full-node |
|||
- [libunicode](https://github.com/contour-terminal/libunicode/): a |
|||
modern C++17 Unicode library |
|||
- [MariaDB](https://mariadb.org/): relational database management |
|||
system |
|||
- [Microsoft Verona](https://github.com/microsoft/verona): research |
|||
programming language for concurrent ownership |
|||
- [MongoDB](https://mongodb.com/): distributed document database |
|||
- [MongoDB Smasher](https://github.com/duckie/mongo_smasher): a small |
|||
tool to generate randomized datasets |
|||
- [OpenSpace](https://openspaceproject.com/): an open-source |
|||
astrovisualization framework |
|||
- [PenUltima Online (POL)](https://www.polserver.com/): an MMO server, |
|||
compatible with most Ultima Online clients |
|||
- [PyTorch](https://github.com/pytorch/pytorch): an open-source |
|||
machine learning library |
|||
- [quasardb](https://www.quasardb.net/): a distributed, |
|||
high-performance, associative database |
|||
- [Quill](https://github.com/odygrd/quill): asynchronous low-latency |
|||
logging library |
|||
- [QKW](https://github.com/ravijanjam/qkw): generalizing aliasing to |
|||
simplify navigation, and execute complex multi-line terminal |
|||
command sequences |
|||
- [redis-cerberus](https://github.com/HunanTV/redis-cerberus): a Redis |
|||
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 |
|||
client library |
|||
- [Salesforce Analytics |
|||
Cloud](https://www.salesforce.com/analytics-cloud/overview/): |
|||
business intelligence software |
|||
- [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 |
|||
C++ framework for high-performance server applications on modern |
|||
hardware |
|||
- [spdlog](https://github.com/gabime/spdlog): super fast C++ logging |
|||
library |
|||
- [Stellar](https://www.stellar.org/): financial platform |
|||
- [Touch Surgery](https://www.touchsurgery.com/): surgery simulator |
|||
- [TrinityCore](https://github.com/TrinityCore/TrinityCore): |
|||
open-source MMORPG framework |
|||
- [🐙 userver framework](https://userver.tech/): open-source |
|||
asynchronous framework with a rich set of abstractions and database |
|||
drivers |
|||
- [Windows Terminal](https://github.com/microsoft/terminal): the new |
|||
Windows terminal |
|||
|
|||
[More\...](https://github.com/search?q=fmtlib&type=Code) |
|||
|
|||
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/Function-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 |
|||
([vitaut](https://github.com/vitaut)) with contributions from many other |
|||
people. See |
|||
[Contributors](https://github.com/fmtlib/fmt/graphs/contributors) and |
|||
[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. |
3077
thirdparty/fmt/include/fmt/base.h
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
732
thirdparty/fmt/include/fmt/chrono.h
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2925
thirdparty/fmt/include/fmt/core.h
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1697
thirdparty/fmt/include/fmt/format.h
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,15 @@ |
|||
LOCAL_PATH := $(call my-dir) |
|||
include $(CLEAR_VARS) |
|||
|
|||
LOCAL_MODULE := fmt_static |
|||
LOCAL_MODULE_FILENAME := libfmt |
|||
|
|||
LOCAL_SRC_FILES := ../src/format.cc |
|||
|
|||
LOCAL_C_INCLUDES := $(LOCAL_PATH) |
|||
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH) |
|||
|
|||
LOCAL_CFLAGS += -std=c++11 -fexceptions |
|||
|
|||
include $(BUILD_STATIC_LIBRARY) |
|||
|
@ -0,0 +1 @@ |
|||
<manifest package="dev.fmt" /> |
@ -0,0 +1,4 @@ |
|||
This directory contains build support files such as |
|||
|
|||
* CMake modules |
|||
* Build scripts |
@ -0,0 +1,19 @@ |
|||
# -*- mode: ruby -*- |
|||
# vi: set ft=ruby : |
|||
|
|||
# A vagrant config for testing against gcc-4.8. |
|||
Vagrant.configure("2") do |config| |
|||
config.vm.box = "bento/ubuntu-22.04-arm64" |
|||
|
|||
config.vm.provider "vmware_desktop" do |vb| |
|||
vb.memory = "4096" |
|||
end |
|||
|
|||
config.vm.provision "shell", inline: <<-SHELL |
|||
apt-get update |
|||
apt-get install -y g++ make wget git |
|||
wget -q https://github.com/Kitware/CMake/releases/download/v3.26.0/cmake-3.26.0-Linux-x86_64.tar.gz |
|||
tar xzf cmake-3.26.0-Linux-x86_64.tar.gz |
|||
ln -s `pwd`/cmake-3.26.0-Linux-x86_64/bin/cmake /usr/local/bin |
|||
SHELL |
|||
end |
@ -0,0 +1 @@ |
|||
7.1.2 |
@ -0,0 +1,20 @@ |
|||
cc_library( |
|||
name = "fmt", |
|||
srcs = [ |
|||
#"src/fmt.cc", # No C++ module support, yet in Bazel (https://github.com/bazelbuild/bazel/pull/19940) |
|||
"src/format.cc", |
|||
"src/os.cc", |
|||
], |
|||
hdrs = glob([ |
|||
"include/fmt/*.h", |
|||
]), |
|||
copts = select({ |
|||
"@platforms//os:windows": ["-utf-8"], |
|||
"//conditions:default": [], |
|||
}), |
|||
includes = [ |
|||
"include", |
|||
], |
|||
strip_include_prefix = "include", |
|||
visibility = ["//visibility:public"], |
|||
) |
@ -0,0 +1,6 @@ |
|||
module( |
|||
name = "fmt", |
|||
compatibility_level = 10, |
|||
) |
|||
|
|||
bazel_dep(name = "platforms", version = "0.0.10") |
@ -0,0 +1,28 @@ |
|||
# Bazel support |
|||
|
|||
To get [Bazel](https://bazel.build/) working with {fmt} you can copy the files `BUILD.bazel`, |
|||
`MODULE.bazel`, `WORKSPACE.bazel`, and `.bazelversion` from this folder (`support/bazel`) to the root folder of this project. |
|||
This way {fmt} gets bazelized and can be used with Bazel (e.g. doing a `bazel build //...` on {fmt}). |
|||
|
|||
## Using {fmt} as a dependency |
|||
|
|||
### Using Bzlmod |
|||
|
|||
The [Bazel Central Registry](https://github.com/bazelbuild/bazel-central-registry/tree/main/modules/fmt) provides support for {fmt}. |
|||
|
|||
For instance, to use {fmt} add to your `MODULE.bazel` file: |
|||
|
|||
``` |
|||
bazel_dep(name = "fmt", version = "10.2.1") |
|||
``` |
|||
|
|||
### Live at head |
|||
|
|||
For a live-at-head approach, you can copy the contents of this repository and move the Bazel-related build files to the root folder of this project as described above and make use of `local_path_override`, e.g.: |
|||
|
|||
``` |
|||
local_path_override( |
|||
module_name = "fmt", |
|||
path = "../third_party/fmt", |
|||
) |
|||
``` |
@ -0,0 +1,2 @@ |
|||
# WORKSPACE marker file needed by Bazel |
|||
|
@ -0,0 +1,132 @@ |
|||
import java.nio.file.Paths |
|||
|
|||
// General gradle arguments for root project |
|||
buildscript { |
|||
repositories { |
|||
google() |
|||
jcenter() |
|||
} |
|||
dependencies { |
|||
// |
|||
// https://developer.android.com/studio/releases/gradle-plugin#updating-gradle |
|||
// |
|||
// Notice that 4.0.0 here is the version of [Android Gradle Plugin] |
|||
// According to URL above you will need Gradle 6.1 or higher |
|||
// |
|||
classpath "com.android.tools.build:gradle:4.1.1" |
|||
} |
|||
} |
|||
repositories { |
|||
google() |
|||
jcenter() |
|||
} |
|||
|
|||
// Project's root where CMakeLists.txt exists: rootDir/support/.cxx -> rootDir |
|||
def rootDir = Paths.get(project.buildDir.getParent()).getParent() |
|||
println("rootDir: ${rootDir}") |
|||
|
|||
// Output: Shared library (.so) for Android |
|||
apply plugin: "com.android.library" |
|||
android { |
|||
compileSdkVersion 25 // Android 7.0 |
|||
|
|||
// Target ABI |
|||
// - This option controls target platform of module |
|||
// - The platform might be limited by compiler's support |
|||
// some can work with Clang(default), but some can work only with GCC... |
|||
// if bad, both toolchains might not support it |
|||
splits { |
|||
abi { |
|||
enable true |
|||
// Specify platforms for Application |
|||
reset() |
|||
include "arm64-v8a", "armeabi-v7a", "x86_64" |
|||
} |
|||
} |
|||
ndkVersion "21.3.6528147" // ANDROID_NDK_HOME is deprecated. Be explicit |
|||
|
|||
defaultConfig { |
|||
minSdkVersion 21 // Android 5.0+ |
|||
targetSdkVersion 25 // Follow Compile SDK |
|||
versionCode 34 // Follow release count |
|||
versionName "7.1.2" // Follow Official version |
|||
|
|||
externalNativeBuild { |
|||
cmake { |
|||
arguments "-DANDROID_STL=c++_shared" // Specify Android STL |
|||
arguments "-DBUILD_SHARED_LIBS=true" // Build shared object |
|||
arguments "-DFMT_TEST=false" // Skip test |
|||
arguments "-DFMT_DOC=false" // Skip document |
|||
cppFlags "-std=c++17" |
|||
targets "fmt" |
|||
} |
|||
} |
|||
println(externalNativeBuild.cmake.cppFlags) |
|||
println(externalNativeBuild.cmake.arguments) |
|||
} |
|||
|
|||
// External Native build |
|||
// - Use existing CMakeList.txt |
|||
// - Give path to CMake. This gradle file should be |
|||
// neighbor of the top level cmake |
|||
externalNativeBuild { |
|||
cmake { |
|||
version "3.10.0+" |
|||
path "${rootDir}/CMakeLists.txt" |
|||
// buildStagingDirectory "./build" // Custom path for cmake output |
|||
} |
|||
} |
|||
|
|||
sourceSets{ |
|||
// Android Manifest for Gradle |
|||
main { |
|||
manifest.srcFile "AndroidManifest.xml" |
|||
} |
|||
} |
|||
|
|||
// https://developer.android.com/studio/build/native-dependencies#build_system_configuration |
|||
buildFeatures { |
|||
prefab true |
|||
prefabPublishing true |
|||
} |
|||
prefab { |
|||
fmt { |
|||
headers "${rootDir}/include" |
|||
} |
|||
} |
|||
} |
|||
|
|||
assemble.doLast |
|||
{ |
|||
// Instead of `ninja install`, Gradle will deploy the files. |
|||
// We are doing this since FMT is dependent to the ANDROID_STL after build |
|||
copy { |
|||
from "build/intermediates/cmake" |
|||
into "${rootDir}/libs" |
|||
} |
|||
// Copy debug binaries |
|||
copy { |
|||
from "${rootDir}/libs/debug/obj" |
|||
into "${rootDir}/libs/debug" |
|||
} |
|||
// Copy Release binaries |
|||
copy { |
|||
from "${rootDir}/libs/release/obj" |
|||
into "${rootDir}/libs/release" |
|||
} |
|||
// Remove empty directory |
|||
delete "${rootDir}/libs/debug/obj" |
|||
delete "${rootDir}/libs/release/obj" |
|||
|
|||
// Copy AAR files. Notice that the aar is named after the folder of this script. |
|||
copy { |
|||
from "build/outputs/aar/support-release.aar" |
|||
into "${rootDir}/libs" |
|||
rename "support-release.aar", "fmt-release.aar" |
|||
} |
|||
copy { |
|||
from "build/outputs/aar/support-debug.aar" |
|||
into "${rootDir}/libs" |
|||
rename "support-debug.aar", "fmt-debug.aar" |
|||
} |
|||
} |
@ -0,0 +1,43 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
"""Compile source on a range of commits |
|||
|
|||
Usage: |
|||
check-commits <start> <source> |
|||
""" |
|||
|
|||
import docopt, os, sys, tempfile |
|||
from subprocess import check_call, check_output, run |
|||
|
|||
args = docopt.docopt(__doc__) |
|||
start = args.get('<start>') |
|||
source = args.get('<source>') |
|||
|
|||
cwd = os.getcwd() |
|||
|
|||
with tempfile.TemporaryDirectory() as work_dir: |
|||
check_call(['git', 'clone', 'https://github.com/fmtlib/fmt.git'], |
|||
cwd=work_dir) |
|||
repo_dir = os.path.join(work_dir, 'fmt') |
|||
commits = check_output( |
|||
['git', 'rev-list', f'{start}..HEAD', '--abbrev-commit', |
|||
'--', 'include', 'src'], |
|||
text=True, cwd=repo_dir).rstrip().split('\n') |
|||
commits.reverse() |
|||
print('Time\tCommit') |
|||
for commit in commits: |
|||
check_call(['git', '-c', 'advice.detachedHead=false', 'checkout', commit], |
|||
cwd=repo_dir) |
|||
returncode = run( |
|||
['c++', '-std=c++11', '-O3', '-DNDEBUG', '-I', 'include', |
|||
'src/format.cc', os.path.join(cwd, source)], cwd=repo_dir).returncode |
|||
if returncode != 0: |
|||
continue |
|||
times = [] |
|||
for i in range(5): |
|||
output = check_output([os.path.join(repo_dir, 'a.out')], text=True) |
|||
times.append(float(output)) |
|||
message = check_output(['git', 'log', '-1', '--pretty=format:%s', commit], |
|||
cwd=repo_dir, text=True) |
|||
print(f'{min(times)}\t{commit} {message[:40]}') |
|||
sys.stdout.flush() |
@ -1,54 +0,0 @@ |
|||
# C++14 feature support detection |
|||
|
|||
include(CheckCXXCompilerFlag) |
|||
function (fmt_check_cxx_compiler_flag flag result) |
|||
if (NOT MSVC) |
|||
check_cxx_compiler_flag("${flag}" ${result}) |
|||
endif () |
|||
endfunction () |
|||
|
|||
if (NOT CMAKE_CXX_STANDARD) |
|||
set(CMAKE_CXX_STANDARD 11) |
|||
endif() |
|||
message(STATUS "CXX_STANDARD: ${CMAKE_CXX_STANDARD}") |
|||
|
|||
if (CMAKE_CXX_STANDARD EQUAL 20) |
|||
fmt_check_cxx_compiler_flag(-std=c++20 has_std_20_flag) |
|||
fmt_check_cxx_compiler_flag(-std=c++2a has_std_2a_flag) |
|||
|
|||
if (has_std_20_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++20) |
|||
elseif (has_std_2a_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++2a) |
|||
endif () |
|||
|
|||
elseif (CMAKE_CXX_STANDARD EQUAL 17) |
|||
fmt_check_cxx_compiler_flag(-std=c++17 has_std_17_flag) |
|||
fmt_check_cxx_compiler_flag(-std=c++1z has_std_1z_flag) |
|||
|
|||
if (has_std_17_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++17) |
|||
elseif (has_std_1z_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++1z) |
|||
endif () |
|||
|
|||
elseif (CMAKE_CXX_STANDARD EQUAL 14) |
|||
fmt_check_cxx_compiler_flag(-std=c++14 has_std_14_flag) |
|||
fmt_check_cxx_compiler_flag(-std=c++1y has_std_1y_flag) |
|||
|
|||
if (has_std_14_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++14) |
|||
elseif (has_std_1y_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++1y) |
|||
endif () |
|||
|
|||
elseif (CMAKE_CXX_STANDARD EQUAL 11) |
|||
fmt_check_cxx_compiler_flag(-std=c++11 has_std_11_flag) |
|||
fmt_check_cxx_compiler_flag(-std=c++0x has_std_0x_flag) |
|||
|
|||
if (has_std_11_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++11) |
|||
elseif (has_std_0x_flag) |
|||
set(CXX_STANDARD_FLAG -std=c++0x) |
|||
endif () |
|||
endif () |
@ -0,0 +1,581 @@ |
|||
"""Pythonic command-line interface parser that will make you smile. |
|||
|
|||
* http://docopt.org |
|||
* Repository and issue-tracker: https://github.com/docopt/docopt |
|||
* Licensed under terms of MIT license (see LICENSE-MIT) |
|||
* Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com |
|||
|
|||
""" |
|||
import sys |
|||
import re |
|||
|
|||
|
|||
__all__ = ['docopt'] |
|||
__version__ = '0.6.1' |
|||
|
|||
|
|||
class DocoptLanguageError(Exception): |
|||
|
|||
"""Error in construction of usage-message by developer.""" |
|||
|
|||
|
|||
class DocoptExit(SystemExit): |
|||
|
|||
"""Exit in case user invoked program with incorrect arguments.""" |
|||
|
|||
usage = '' |
|||
|
|||
def __init__(self, message=''): |
|||
SystemExit.__init__(self, (message + '\n' + self.usage).strip()) |
|||
|
|||
|
|||
class Pattern(object): |
|||
|
|||
def __eq__(self, other): |
|||
return repr(self) == repr(other) |
|||
|
|||
def __hash__(self): |
|||
return hash(repr(self)) |
|||
|
|||
def fix(self): |
|||
self.fix_identities() |
|||
self.fix_repeating_arguments() |
|||
return self |
|||
|
|||
def fix_identities(self, uniq=None): |
|||
"""Make pattern-tree tips point to same object if they are equal.""" |
|||
if not hasattr(self, 'children'): |
|||
return self |
|||
uniq = list(set(self.flat())) if uniq is None else uniq |
|||
for i, child in enumerate(self.children): |
|||
if not hasattr(child, 'children'): |
|||
assert child in uniq |
|||
self.children[i] = uniq[uniq.index(child)] |
|||
else: |
|||
child.fix_identities(uniq) |
|||
|
|||
def fix_repeating_arguments(self): |
|||
"""Fix elements that should accumulate/increment values.""" |
|||
either = [list(child.children) for child in transform(self).children] |
|||
for case in either: |
|||
for e in [child for child in case if case.count(child) > 1]: |
|||
if type(e) is Argument or type(e) is Option and e.argcount: |
|||
if e.value is None: |
|||
e.value = [] |
|||
elif type(e.value) is not list: |
|||
e.value = e.value.split() |
|||
if type(e) is Command or type(e) is Option and e.argcount == 0: |
|||
e.value = 0 |
|||
return self |
|||
|
|||
|
|||
def transform(pattern): |
|||
"""Expand pattern into an (almost) equivalent one, but with single Either. |
|||
|
|||
Example: ((-a | -b) (-c | -d)) => (-a -c | -a -d | -b -c | -b -d) |
|||
Quirks: [-a] => (-a), (-a...) => (-a -a) |
|||
|
|||
""" |
|||
result = [] |
|||
groups = [[pattern]] |
|||
while groups: |
|||
children = groups.pop(0) |
|||
parents = [Required, Optional, OptionsShortcut, Either, OneOrMore] |
|||
if any(t in map(type, children) for t in parents): |
|||
child = [c for c in children if type(c) in parents][0] |
|||
children.remove(child) |
|||
if type(child) is Either: |
|||
for c in child.children: |
|||
groups.append([c] + children) |
|||
elif type(child) is OneOrMore: |
|||
groups.append(child.children * 2 + children) |
|||
else: |
|||
groups.append(child.children + children) |
|||
else: |
|||
result.append(children) |
|||
return Either(*[Required(*e) for e in result]) |
|||
|
|||
|
|||
class LeafPattern(Pattern): |
|||
|
|||
"""Leaf/terminal node of a pattern tree.""" |
|||
|
|||
def __init__(self, name, value=None): |
|||
self.name, self.value = name, value |
|||
|
|||
def __repr__(self): |
|||
return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) |
|||
|
|||
def flat(self, *types): |
|||
return [self] if not types or type(self) in types else [] |
|||
|
|||
def match(self, left, collected=None): |
|||
collected = [] if collected is None else collected |
|||
pos, match = self.single_match(left) |
|||
if match is None: |
|||
return False, left, collected |
|||
left_ = left[:pos] + left[pos + 1:] |
|||
same_name = [a for a in collected if a.name == self.name] |
|||
if type(self.value) in (int, list): |
|||
if type(self.value) is int: |
|||
increment = 1 |
|||
else: |
|||
increment = ([match.value] if type(match.value) is str |
|||
else match.value) |
|||
if not same_name: |
|||
match.value = increment |
|||
return True, left_, collected + [match] |
|||
same_name[0].value += increment |
|||
return True, left_, collected |
|||
return True, left_, collected + [match] |
|||
|
|||
|
|||
class BranchPattern(Pattern): |
|||
|
|||
"""Branch/inner node of a pattern tree.""" |
|||
|
|||
def __init__(self, *children): |
|||
self.children = list(children) |
|||
|
|||
def __repr__(self): |
|||
return '%s(%s)' % (self.__class__.__name__, |
|||
', '.join(repr(a) for a in self.children)) |
|||
|
|||
def flat(self, *types): |
|||
if type(self) in types: |
|||
return [self] |
|||
return sum([child.flat(*types) for child in self.children], []) |
|||
|
|||
|
|||
class Argument(LeafPattern): |
|||
|
|||
def single_match(self, left): |
|||
for n, pattern in enumerate(left): |
|||
if type(pattern) is Argument: |
|||
return n, Argument(self.name, pattern.value) |
|||
return None, None |
|||
|
|||
@classmethod |
|||
def parse(class_, source): |
|||
name = re.findall('(<\S*?>)', source)[0] |
|||
value = re.findall('\[default: (.*)\]', source, flags=re.I) |
|||
return class_(name, value[0] if value else None) |
|||
|
|||
|
|||
class Command(Argument): |
|||
|
|||
def __init__(self, name, value=False): |
|||
self.name, self.value = name, value |
|||
|
|||
def single_match(self, left): |
|||
for n, pattern in enumerate(left): |
|||
if type(pattern) is Argument: |
|||
if pattern.value == self.name: |
|||
return n, Command(self.name, True) |
|||
else: |
|||
break |
|||
return None, None |
|||
|
|||
|
|||
class Option(LeafPattern): |
|||
|
|||
def __init__(self, short=None, long=None, argcount=0, value=False): |
|||
assert argcount in (0, 1) |
|||
self.short, self.long, self.argcount = short, long, argcount |
|||
self.value = None if value is False and argcount else value |
|||
|
|||
@classmethod |
|||
def parse(class_, option_description): |
|||
short, long, argcount, value = None, None, 0, False |
|||
options, _, description = option_description.strip().partition(' ') |
|||
options = options.replace(',', ' ').replace('=', ' ') |
|||
for s in options.split(): |
|||
if s.startswith('--'): |
|||
long = s |
|||
elif s.startswith('-'): |
|||
short = s |
|||
else: |
|||
argcount = 1 |
|||
if argcount: |
|||
matched = re.findall('\[default: (.*)\]', description, flags=re.I) |
|||
value = matched[0] if matched else None |
|||
return class_(short, long, argcount, value) |
|||
|
|||
def single_match(self, left): |
|||
for n, pattern in enumerate(left): |
|||
if self.name == pattern.name: |
|||
return n, pattern |
|||
return None, None |
|||
|
|||
@property |
|||
def name(self): |
|||
return self.long or self.short |
|||
|
|||
def __repr__(self): |
|||
return 'Option(%r, %r, %r, %r)' % (self.short, self.long, |
|||
self.argcount, self.value) |
|||
|
|||
|
|||
class Required(BranchPattern): |
|||
|
|||
def match(self, left, collected=None): |
|||
collected = [] if collected is None else collected |
|||
l = left |
|||
c = collected |
|||
for pattern in self.children: |
|||
matched, l, c = pattern.match(l, c) |
|||
if not matched: |
|||
return False, left, collected |
|||
return True, l, c |
|||
|
|||
|
|||
class Optional(BranchPattern): |
|||
|
|||
def match(self, left, collected=None): |
|||
collected = [] if collected is None else collected |
|||
for pattern in self.children: |
|||
m, left, collected = pattern.match(left, collected) |
|||
return True, left, collected |
|||
|
|||
|
|||
class OptionsShortcut(Optional): |
|||
|
|||
"""Marker/placeholder for [options] shortcut.""" |
|||
|
|||
|
|||
class OneOrMore(BranchPattern): |
|||
|
|||
def match(self, left, collected=None): |
|||
assert len(self.children) == 1 |
|||
collected = [] if collected is None else collected |
|||
l = left |
|||
c = collected |
|||
l_ = None |
|||
matched = True |
|||
times = 0 |
|||
while matched: |
|||
# could it be that something didn't match but changed l or c? |
|||
matched, l, c = self.children[0].match(l, c) |
|||
times += 1 if matched else 0 |
|||
if l_ == l: |
|||
break |
|||
l_ = l |
|||
if times >= 1: |
|||
return True, l, c |
|||
return False, left, collected |
|||
|
|||
|
|||
class Either(BranchPattern): |
|||
|
|||
def match(self, left, collected=None): |
|||
collected = [] if collected is None else collected |
|||
outcomes = [] |
|||
for pattern in self.children: |
|||
matched, _, _ = outcome = pattern.match(left, collected) |
|||
if matched: |
|||
outcomes.append(outcome) |
|||
if outcomes: |
|||
return min(outcomes, key=lambda outcome: len(outcome[1])) |
|||
return False, left, collected |
|||
|
|||
|
|||
class Tokens(list): |
|||
|
|||
def __init__(self, source, error=DocoptExit): |
|||
self += source.split() if hasattr(source, 'split') else source |
|||
self.error = error |
|||
|
|||
@staticmethod |
|||
def from_pattern(source): |
|||
source = re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source) |
|||
source = [s for s in re.split('\s+|(\S*<.*?>)', source) if s] |
|||
return Tokens(source, error=DocoptLanguageError) |
|||
|
|||
def move(self): |
|||
return self.pop(0) if len(self) else None |
|||
|
|||
def current(self): |
|||
return self[0] if len(self) else None |
|||
|
|||
|
|||
def parse_long(tokens, options): |
|||
"""long ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" |
|||
long, eq, value = tokens.move().partition('=') |
|||
assert long.startswith('--') |
|||
value = None if eq == value == '' else value |
|||
similar = [o for o in options if o.long == long] |
|||
if tokens.error is DocoptExit and similar == []: # if no exact match |
|||
similar = [o for o in options if o.long and o.long.startswith(long)] |
|||
if len(similar) > 1: # might be simply specified ambiguously 2+ times? |
|||
raise tokens.error('%s is not a unique prefix: %s?' % |
|||
(long, ', '.join(o.long for o in similar))) |
|||
elif len(similar) < 1: |
|||
argcount = 1 if eq == '=' else 0 |
|||
o = Option(None, long, argcount) |
|||
options.append(o) |
|||
if tokens.error is DocoptExit: |
|||
o = Option(None, long, argcount, value if argcount else True) |
|||
else: |
|||
o = Option(similar[0].short, similar[0].long, |
|||
similar[0].argcount, similar[0].value) |
|||
if o.argcount == 0: |
|||
if value is not None: |
|||
raise tokens.error('%s must not have an argument' % o.long) |
|||
else: |
|||
if value is None: |
|||
if tokens.current() in [None, '--']: |
|||
raise tokens.error('%s requires argument' % o.long) |
|||
value = tokens.move() |
|||
if tokens.error is DocoptExit: |
|||
o.value = value if value is not None else True |
|||
return [o] |
|||
|
|||
|
|||
def parse_shorts(tokens, options): |
|||
"""shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" |
|||
token = tokens.move() |
|||
assert token.startswith('-') and not token.startswith('--') |
|||
left = token.lstrip('-') |
|||
parsed = [] |
|||
while left != '': |
|||
short, left = '-' + left[0], left[1:] |
|||
similar = [o for o in options if o.short == short] |
|||
if len(similar) > 1: |
|||
raise tokens.error('%s is specified ambiguously %d times' % |
|||
(short, len(similar))) |
|||
elif len(similar) < 1: |
|||
o = Option(short, None, 0) |
|||
options.append(o) |
|||
if tokens.error is DocoptExit: |
|||
o = Option(short, None, 0, True) |
|||
else: # why copying is necessary here? |
|||
o = Option(short, similar[0].long, |
|||
similar[0].argcount, similar[0].value) |
|||
value = None |
|||
if o.argcount != 0: |
|||
if left == '': |
|||
if tokens.current() in [None, '--']: |
|||
raise tokens.error('%s requires argument' % short) |
|||
value = tokens.move() |
|||
else: |
|||
value = left |
|||
left = '' |
|||
if tokens.error is DocoptExit: |
|||
o.value = value if value is not None else True |
|||
parsed.append(o) |
|||
return parsed |
|||
|
|||
|
|||
def parse_pattern(source, options): |
|||
tokens = Tokens.from_pattern(source) |
|||
result = parse_expr(tokens, options) |
|||
if tokens.current() is not None: |
|||
raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) |
|||
return Required(*result) |
|||
|
|||
|
|||
def parse_expr(tokens, options): |
|||
"""expr ::= seq ( '|' seq )* ;""" |
|||
seq = parse_seq(tokens, options) |
|||
if tokens.current() != '|': |
|||
return seq |
|||
result = [Required(*seq)] if len(seq) > 1 else seq |
|||
while tokens.current() == '|': |
|||
tokens.move() |
|||
seq = parse_seq(tokens, options) |
|||
result += [Required(*seq)] if len(seq) > 1 else seq |
|||
return [Either(*result)] if len(result) > 1 else result |
|||
|
|||
|
|||
def parse_seq(tokens, options): |
|||
"""seq ::= ( atom [ '...' ] )* ;""" |
|||
result = [] |
|||
while tokens.current() not in [None, ']', ')', '|']: |
|||
atom = parse_atom(tokens, options) |
|||
if tokens.current() == '...': |
|||
atom = [OneOrMore(*atom)] |
|||
tokens.move() |
|||
result += atom |
|||
return result |
|||
|
|||
|
|||
def parse_atom(tokens, options): |
|||
"""atom ::= '(' expr ')' | '[' expr ']' | 'options' |
|||
| long | shorts | argument | command ; |
|||
""" |
|||
token = tokens.current() |
|||
result = [] |
|||
if token in '([': |
|||
tokens.move() |
|||
matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token] |
|||
result = pattern(*parse_expr(tokens, options)) |
|||
if tokens.move() != matching: |
|||
raise tokens.error("unmatched '%s'" % token) |
|||
return [result] |
|||
elif token == 'options': |
|||
tokens.move() |
|||
return [OptionsShortcut()] |
|||
elif token.startswith('--') and token != '--': |
|||
return parse_long(tokens, options) |
|||
elif token.startswith('-') and token not in ('-', '--'): |
|||
return parse_shorts(tokens, options) |
|||
elif token.startswith('<') and token.endswith('>') or token.isupper(): |
|||
return [Argument(tokens.move())] |
|||
else: |
|||
return [Command(tokens.move())] |
|||
|
|||
|
|||
def parse_argv(tokens, options, options_first=False): |
|||
"""Parse command-line argument vector. |
|||
|
|||
If options_first: |
|||
argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; |
|||
else: |
|||
argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; |
|||
|
|||
""" |
|||
parsed = [] |
|||
while tokens.current() is not None: |
|||
if tokens.current() == '--': |
|||
return parsed + [Argument(None, v) for v in tokens] |
|||
elif tokens.current().startswith('--'): |
|||
parsed += parse_long(tokens, options) |
|||
elif tokens.current().startswith('-') and tokens.current() != '-': |
|||
parsed += parse_shorts(tokens, options) |
|||
elif options_first: |
|||
return parsed + [Argument(None, v) for v in tokens] |
|||
else: |
|||
parsed.append(Argument(None, tokens.move())) |
|||
return parsed |
|||
|
|||
|
|||
def parse_defaults(doc): |
|||
defaults = [] |
|||
for s in parse_section('options:', doc): |
|||
# FIXME corner case "bla: options: --foo" |
|||
_, _, s = s.partition(':') # get rid of "options:" |
|||
split = re.split('\n[ \t]*(-\S+?)', '\n' + s)[1:] |
|||
split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] |
|||
options = [Option.parse(s) for s in split if s.startswith('-')] |
|||
defaults += options |
|||
return defaults |
|||
|
|||
|
|||
def parse_section(name, source): |
|||
pattern = re.compile('^([^\n]*' + name + '[^\n]*\n?(?:[ \t].*?(?:\n|$))*)', |
|||
re.IGNORECASE | re.MULTILINE) |
|||
return [s.strip() for s in pattern.findall(source)] |
|||
|
|||
|
|||
def formal_usage(section): |
|||
_, _, section = section.partition(':') # drop "usage:" |
|||
pu = section.split() |
|||
return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' |
|||
|
|||
|
|||
def extras(help, version, options, doc): |
|||
if help and any((o.name in ('-h', '--help')) and o.value for o in options): |
|||
print(doc.strip("\n")) |
|||
sys.exit() |
|||
if version and any(o.name == '--version' and o.value for o in options): |
|||
print(version) |
|||
sys.exit() |
|||
|
|||
|
|||
class Dict(dict): |
|||
def __repr__(self): |
|||
return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items())) |
|||
|
|||
|
|||
def docopt(doc, argv=None, help=True, version=None, options_first=False): |
|||
"""Parse `argv` based on command-line interface described in `doc`. |
|||
|
|||
`docopt` creates your command-line interface based on its |
|||
description that you pass as `doc`. Such description can contain |
|||
--options, <positional-argument>, commands, which could be |
|||
[optional], (required), (mutually | exclusive) or repeated... |
|||
|
|||
Parameters |
|||
---------- |
|||
doc : str |
|||
Description of your command-line interface. |
|||
argv : list of str, optional |
|||
Argument vector to be parsed. sys.argv[1:] is used if not |
|||
provided. |
|||
help : bool (default: True) |
|||
Set to False to disable automatic help on -h or --help |
|||
options. |
|||
version : any object |
|||
If passed, the object will be printed if --version is in |
|||
`argv`. |
|||
options_first : bool (default: False) |
|||
Set to True to require options precede positional arguments, |
|||
i.e. to forbid options and positional arguments intermix. |
|||
|
|||
Returns |
|||
------- |
|||
args : dict |
|||
A dictionary, where keys are names of command-line elements |
|||
such as e.g. "--verbose" and "<path>", and values are the |
|||
parsed values of those elements. |
|||
|
|||
Example |
|||
------- |
|||
>>> from docopt import docopt |
|||
>>> doc = ''' |
|||
... Usage: |
|||
... my_program tcp <host> <port> [--timeout=<seconds>] |
|||
... my_program serial <port> [--baud=<n>] [--timeout=<seconds>] |
|||
... my_program (-h | --help | --version) |
|||
... |
|||
... Options: |
|||
... -h, --help Show this screen and exit. |
|||
... --baud=<n> Baudrate [default: 9600] |
|||
... ''' |
|||
>>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] |
|||
>>> docopt(doc, argv) |
|||
{'--baud': '9600', |
|||
'--help': False, |
|||
'--timeout': '30', |
|||
'--version': False, |
|||
'<host>': '127.0.0.1', |
|||
'<port>': '80', |
|||
'serial': False, |
|||
'tcp': True} |
|||
|
|||
See also |
|||
-------- |
|||
* For video introduction see http://docopt.org |
|||
* Full documentation is available in README.rst as well as online |
|||
at https://github.com/docopt/docopt#readme |
|||
|
|||
""" |
|||
argv = sys.argv[1:] if argv is None else argv |
|||
|
|||
usage_sections = parse_section('usage:', doc) |
|||
if len(usage_sections) == 0: |
|||
raise DocoptLanguageError('"usage:" (case-insensitive) not found.') |
|||
if len(usage_sections) > 1: |
|||
raise DocoptLanguageError('More than one "usage:" (case-insensitive).') |
|||
DocoptExit.usage = usage_sections[0] |
|||
|
|||
options = parse_defaults(doc) |
|||
pattern = parse_pattern(formal_usage(DocoptExit.usage), options) |
|||
# [default] syntax for argument is disabled |
|||
#for a in pattern.flat(Argument): |
|||
# same_name = [d for d in arguments if d.name == a.name] |
|||
# if same_name: |
|||
# a.value = same_name[0].value |
|||
argv = parse_argv(Tokens(argv), list(options), options_first) |
|||
pattern_options = set(pattern.flat(Option)) |
|||
for options_shortcut in pattern.flat(OptionsShortcut): |
|||
doc_options = parse_defaults(doc) |
|||
options_shortcut.children = list(set(doc_options) - pattern_options) |
|||
#if any_options: |
|||
# options_shortcut.children += [Option(o.short, o.long, o.argcount) |
|||
# for o in argv if type(o) is Option] |
|||
extras(help, version, argv, doc) |
|||
matched, left, collected = pattern.fix().match(argv) |
|||
if matched and left == []: # better error message if left? |
|||
return Dict((a.name, a.value) for a in (pattern.flat() + collected)) |
|||
raise DocoptExit() |
@ -0,0 +1,218 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
"""Manage site and releases. |
|||
|
|||
Usage: |
|||
manage.py release [<branch>] |
|||
manage.py site |
|||
|
|||
For the release command $FMT_TOKEN should contain a GitHub personal access token |
|||
obtained from https://github.com/settings/tokens. |
|||
""" |
|||
|
|||
from __future__ import print_function |
|||
import datetime, docopt, errno, fileinput, json, os |
|||
import re, requests, shutil, sys |
|||
from contextlib import contextmanager |
|||
from subprocess import check_call |
|||
|
|||
|
|||
class Git: |
|||
def __init__(self, dir): |
|||
self.dir = dir |
|||
|
|||
def call(self, method, args, **kwargs): |
|||
return check_call(['git', method] + list(args), **kwargs) |
|||
|
|||
def add(self, *args): |
|||
return self.call('add', args, cwd=self.dir) |
|||
|
|||
def checkout(self, *args): |
|||
return self.call('checkout', args, cwd=self.dir) |
|||
|
|||
def clean(self, *args): |
|||
return self.call('clean', args, cwd=self.dir) |
|||
|
|||
def clone(self, *args): |
|||
return self.call('clone', list(args) + [self.dir]) |
|||
|
|||
def commit(self, *args): |
|||
return self.call('commit', args, cwd=self.dir) |
|||
|
|||
def pull(self, *args): |
|||
return self.call('pull', args, cwd=self.dir) |
|||
|
|||
def push(self, *args): |
|||
return self.call('push', args, cwd=self.dir) |
|||
|
|||
def reset(self, *args): |
|||
return self.call('reset', args, cwd=self.dir) |
|||
|
|||
def update(self, *args): |
|||
clone = not os.path.exists(self.dir) |
|||
if clone: |
|||
self.clone(*args) |
|||
return clone |
|||
|
|||
|
|||
def clean_checkout(repo, branch): |
|||
repo.clean('-f', '-d') |
|||
repo.reset('--hard') |
|||
repo.checkout(branch) |
|||
|
|||
|
|||
class Runner: |
|||
def __init__(self, cwd): |
|||
self.cwd = cwd |
|||
|
|||
def __call__(self, *args, **kwargs): |
|||
kwargs['cwd'] = kwargs.get('cwd', self.cwd) |
|||
check_call(args, **kwargs) |
|||
|
|||
|
|||
def create_build_env(): |
|||
"""Create a build environment.""" |
|||
class Env: |
|||
pass |
|||
env = Env() |
|||
env.fmt_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
|||
env.build_dir = 'build' |
|||
env.fmt_repo = Git(os.path.join(env.build_dir, 'fmt')) |
|||
return env |
|||
|
|||
|
|||
fmt_repo_url = 'git@github.com:fmtlib/fmt' |
|||
|
|||
|
|||
def update_site(env): |
|||
env.fmt_repo.update(fmt_repo_url) |
|||
|
|||
doc_repo = Git(os.path.join(env.build_dir, 'fmt.dev')) |
|||
doc_repo.update('git@github.com:fmtlib/fmt.dev') |
|||
|
|||
version = '11.0.0' |
|||
clean_checkout(env.fmt_repo, version) |
|||
target_doc_dir = os.path.join(env.fmt_repo.dir, 'doc') |
|||
|
|||
# Build the docs. |
|||
html_dir = os.path.join(env.build_dir, 'html') |
|||
if os.path.exists(html_dir): |
|||
shutil.rmtree(html_dir) |
|||
include_dir = env.fmt_repo.dir |
|||
import build |
|||
build.build_docs(version, doc_dir=target_doc_dir, |
|||
include_dir=include_dir, work_dir=env.build_dir) |
|||
shutil.rmtree(os.path.join(html_dir, '.doctrees')) |
|||
# Copy docs to the website. |
|||
version_doc_dir = os.path.join(doc_repo.dir, version) |
|||
try: |
|||
shutil.rmtree(version_doc_dir) |
|||
except OSError as e: |
|||
if e.errno != errno.ENOENT: |
|||
raise |
|||
shutil.move(html_dir, version_doc_dir) |
|||
|
|||
|
|||
def release(args): |
|||
env = create_build_env() |
|||
fmt_repo = env.fmt_repo |
|||
|
|||
branch = args.get('<branch>') |
|||
if branch is None: |
|||
branch = 'master' |
|||
if not fmt_repo.update('-b', branch, fmt_repo_url): |
|||
clean_checkout(fmt_repo, branch) |
|||
|
|||
# Update the date in the changelog and extract the version and the first |
|||
# section content. |
|||
changelog = 'ChangeLog.md' |
|||
changelog_path = os.path.join(fmt_repo.dir, changelog) |
|||
is_first_section = True |
|||
first_section = [] |
|||
for i, line in enumerate(fileinput.input(changelog_path, inplace=True)): |
|||
if i == 0: |
|||
version = re.match(r'# (.*) - TBD', line).group(1) |
|||
line = '# {} - {}\n'.format( |
|||
version, datetime.date.today().isoformat()) |
|||
elif not is_first_section: |
|||
pass |
|||
elif line.startswith('#'): |
|||
is_first_section = False |
|||
else: |
|||
first_section.append(line) |
|||
sys.stdout.write(line) |
|||
if first_section[0] == '\n': |
|||
first_section.pop(0) |
|||
|
|||
ns_version = None |
|||
base_h_path = os.path.join(fmt_repo.dir, 'include', 'fmt', 'base.h') |
|||
for line in fileinput.input(base_h_path): |
|||
m = re.match(r'\s*inline namespace v(.*) .*', line) |
|||
if m: |
|||
ns_version = m.group(1) |
|||
break |
|||
major_version = version.split('.')[0] |
|||
if not ns_version or ns_version != major_version: |
|||
raise Exception(f'Version mismatch {ns_version} != {major_version}') |
|||
|
|||
# Workaround GitHub-flavored Markdown treating newlines as <br>. |
|||
changes = '' |
|||
code_block = False |
|||
stripped = False |
|||
for line in first_section: |
|||
if re.match(r'^\s*```', line): |
|||
code_block = not code_block |
|||
changes += line |
|||
stripped = False |
|||
continue |
|||
if code_block: |
|||
changes += line |
|||
continue |
|||
if line == '\n' or re.match(r'^\s*\|.*', line): |
|||
if stripped: |
|||
changes += '\n' |
|||
stripped = False |
|||
changes += line |
|||
continue |
|||
if stripped: |
|||
line = ' ' + line.lstrip() |
|||
changes += line.rstrip() |
|||
stripped = True |
|||
|
|||
fmt_repo.checkout('-B', 'release') |
|||
fmt_repo.add(changelog) |
|||
fmt_repo.commit('-m', 'Update version') |
|||
|
|||
# Build the docs and package. |
|||
run = Runner(fmt_repo.dir) |
|||
run('cmake', '.') |
|||
run('make', 'doc', 'package_source') |
|||
|
|||
# Create a release on GitHub. |
|||
fmt_repo.push('origin', 'release') |
|||
auth_headers = {'Authorization': 'token ' + os.getenv('FMT_TOKEN')} |
|||
r = requests.post('https://api.github.com/repos/fmtlib/fmt/releases', |
|||
headers=auth_headers, |
|||
data=json.dumps({'tag_name': version, |
|||
'target_commitish': 'release', |
|||
'body': changes, 'draft': True})) |
|||
if r.status_code != 201: |
|||
raise Exception('Failed to create a release ' + str(r)) |
|||
id = r.json()['id'] |
|||
uploads_url = 'https://uploads.github.com/repos/fmtlib/fmt/releases' |
|||
package = 'fmt-{}.zip'.format(version) |
|||
r = requests.post( |
|||
'{}/{}/assets?name={}'.format(uploads_url, id, package), |
|||
headers={'Content-Type': 'application/zip'} | auth_headers, |
|||
data=open('build/fmt/' + package, 'rb')) |
|||
if r.status_code != 201: |
|||
raise Exception('Failed to upload an asset ' + str(r)) |
|||
|
|||
update_site(env) |
|||
|
|||
if __name__ == '__main__': |
|||
args = docopt.docopt(__doc__) |
|||
if args.get('release'): |
|||
release(args) |
|||
elif args.get('site'): |
|||
update_site(create_build_env()) |
@ -0,0 +1,44 @@ |
|||
#!/usr/bin/env python3 |
|||
# A script to invoke mkdocs with the correct environment. |
|||
# Additionally supports deploying via mike: |
|||
# ./mkdocs deploy [mike-deploy-options] |
|||
|
|||
import errno, os, shutil, sys |
|||
from subprocess import call |
|||
|
|||
support_dir = os.path.dirname(os.path.normpath(__file__)) |
|||
build_dir = os.path.join(os.path.dirname(support_dir), 'build') |
|||
|
|||
# Set PYTHONPATH for the mkdocstrings handler. |
|||
env = os.environ.copy() |
|||
path = env.get('PYTHONPATH') |
|||
env['PYTHONPATH'] = \ |
|||
(path + ':' if path else '') + os.path.join(support_dir, 'python') |
|||
|
|||
config_path = os.path.join(support_dir, 'mkdocs.yml') |
|||
args = sys.argv[1:] |
|||
if len(args) > 0: |
|||
command = args[0] |
|||
if command == 'deploy': |
|||
git_url = 'https://github.com/' if 'CI' in os.environ else 'git@github.com:' |
|||
site_repo = git_url + 'fmtlib/fmt.dev.git' |
|||
|
|||
site_dir = os. path.join(build_dir, 'fmt.dev') |
|||
try: |
|||
shutil.rmtree(site_dir) |
|||
except OSError as e: |
|||
if e.errno == errno.ENOENT: |
|||
pass |
|||
ret = call(['git', 'clone', '--depth=1', site_repo, site_dir]) |
|||
if ret != 0: |
|||
sys.exit(ret) |
|||
|
|||
# Copy the config to the build dir because the site is built relative to it. |
|||
config_build_path = os.path.join(build_dir, 'mkdocs.yml') |
|||
shutil.copyfile(config_path, config_build_path) |
|||
|
|||
sys.exit(call(['mike'] + args + ['--config-file', config_build_path, |
|||
'--branch', 'master'], cwd=site_dir, env=env)) |
|||
elif not command.startswith('-'): |
|||
args += ['-f', config_path] |
|||
sys.exit(call(['mkdocs'] + args, env=env)) |
@ -0,0 +1,48 @@ |
|||
site_name: '{fmt}' |
|||
|
|||
docs_dir: ../doc |
|||
|
|||
repo_url: https://github.com/fmtlib/fmt |
|||
|
|||
theme: |
|||
name: material |
|||
features: |
|||
- navigation.tabs |
|||
- navigation.top |
|||
- toc.integrate |
|||
|
|||
extra_javascript: |
|||
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/highlight.min.js |
|||
- fmt.js |
|||
|
|||
extra_css: |
|||
- https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/default.min.css |
|||
- fmt.css |
|||
|
|||
markdown_extensions: |
|||
- pymdownx.highlight: |
|||
# Use JavaScript syntax highlighter instead of Pygments because it |
|||
# automatically applies to code blocks extracted through Doxygen. |
|||
use_pygments: false |
|||
anchor_linenums: true |
|||
line_spans: __span |
|||
pygments_lang_class: true |
|||
- pymdownx.inlinehilite |
|||
- pymdownx.snippets |
|||
|
|||
plugins: |
|||
- search |
|||
- mkdocstrings: |
|||
default_handler: cxx |
|||
nav: |
|||
- Home: index.md |
|||
- Get Started: get-started.md |
|||
- API: api.md |
|||
- Syntax: syntax.md |
|||
|
|||
exclude_docs: ChangeLog-old.md |
|||
|
|||
extra: |
|||
version: |
|||
provider: mike |
|||
generator: false |
@ -0,0 +1,201 @@ |
|||
#!/usr/bin/env python3 |
|||
|
|||
# This script is based on |
|||
# https://github.com/rust-lang/rust/blob/master/library/core/src/unicode/printable.py |
|||
# distributed under https://github.com/rust-lang/rust/blob/master/LICENSE-MIT. |
|||
|
|||
# This script uses the following Unicode tables: |
|||
# - UnicodeData.txt |
|||
|
|||
|
|||
from collections import namedtuple |
|||
import csv |
|||
import os |
|||
import subprocess |
|||
|
|||
NUM_CODEPOINTS=0x110000 |
|||
|
|||
def to_ranges(iter): |
|||
current = None |
|||
for i in iter: |
|||
if current is None or i != current[1] or i in (0x10000, 0x20000): |
|||
if current is not None: |
|||
yield tuple(current) |
|||
current = [i, i + 1] |
|||
else: |
|||
current[1] += 1 |
|||
if current is not None: |
|||
yield tuple(current) |
|||
|
|||
def get_escaped(codepoints): |
|||
for c in codepoints: |
|||
if (c.class_ or "Cn") in "Cc Cf Cs Co Cn Zl Zp Zs".split() and c.value != ord(' '): |
|||
yield c.value |
|||
|
|||
def get_file(f): |
|||
try: |
|||
return open(os.path.basename(f)) |
|||
except FileNotFoundError: |
|||
subprocess.run(["curl", "-O", f], check=True) |
|||
return open(os.path.basename(f)) |
|||
|
|||
Codepoint = namedtuple('Codepoint', 'value class_') |
|||
|
|||
def get_codepoints(f): |
|||
r = csv.reader(f, delimiter=";") |
|||
prev_codepoint = 0 |
|||
class_first = None |
|||
for row in r: |
|||
codepoint = int(row[0], 16) |
|||
name = row[1] |
|||
class_ = row[2] |
|||
|
|||
if class_first is not None: |
|||
if not name.endswith("Last>"): |
|||
raise ValueError("Missing Last after First") |
|||
|
|||
for c in range(prev_codepoint + 1, codepoint): |
|||
yield Codepoint(c, class_first) |
|||
|
|||
class_first = None |
|||
if name.endswith("First>"): |
|||
class_first = class_ |
|||
|
|||
yield Codepoint(codepoint, class_) |
|||
prev_codepoint = codepoint |
|||
|
|||
if class_first is not None: |
|||
raise ValueError("Missing Last after First") |
|||
|
|||
for c in range(prev_codepoint + 1, NUM_CODEPOINTS): |
|||
yield Codepoint(c, None) |
|||
|
|||
def compress_singletons(singletons): |
|||
uppers = [] # (upper, # items in lowers) |
|||
lowers = [] |
|||
|
|||
for i in singletons: |
|||
upper = i >> 8 |
|||
lower = i & 0xff |
|||
if len(uppers) == 0 or uppers[-1][0] != upper: |
|||
uppers.append((upper, 1)) |
|||
else: |
|||
upper, count = uppers[-1] |
|||
uppers[-1] = upper, count + 1 |
|||
lowers.append(lower) |
|||
|
|||
return uppers, lowers |
|||
|
|||
def compress_normal(normal): |
|||
# lengths 0x00..0x7f are encoded as 00, 01, ..., 7e, 7f |
|||
# lengths 0x80..0x7fff are encoded as 80 80, 80 81, ..., ff fe, ff ff |
|||
compressed = [] # [truelen, (truelenaux), falselen, (falselenaux)] |
|||
|
|||
prev_start = 0 |
|||
for start, count in normal: |
|||
truelen = start - prev_start |
|||
falselen = count |
|||
prev_start = start + count |
|||
|
|||
assert truelen < 0x8000 and falselen < 0x8000 |
|||
entry = [] |
|||
if truelen > 0x7f: |
|||
entry.append(0x80 | (truelen >> 8)) |
|||
entry.append(truelen & 0xff) |
|||
else: |
|||
entry.append(truelen & 0x7f) |
|||
if falselen > 0x7f: |
|||
entry.append(0x80 | (falselen >> 8)) |
|||
entry.append(falselen & 0xff) |
|||
else: |
|||
entry.append(falselen & 0x7f) |
|||
|
|||
compressed.append(entry) |
|||
|
|||
return compressed |
|||
|
|||
def print_singletons(uppers, lowers, uppersname, lowersname): |
|||
print(" static constexpr singleton {}[] = {{".format(uppersname)) |
|||
for u, c in uppers: |
|||
print(" {{{:#04x}, {}}},".format(u, c)) |
|||
print(" };") |
|||
print(" static constexpr unsigned char {}[] = {{".format(lowersname)) |
|||
for i in range(0, len(lowers), 8): |
|||
print(" {}".format(" ".join("{:#04x},".format(l) for l in lowers[i:i+8]))) |
|||
print(" };") |
|||
|
|||
def print_normal(normal, normalname): |
|||
print(" static constexpr unsigned char {}[] = {{".format(normalname)) |
|||
for v in normal: |
|||
print(" {}".format(" ".join("{:#04x},".format(i) for i in v))) |
|||
print(" };") |
|||
|
|||
def main(): |
|||
file = get_file("https://www.unicode.org/Public/UNIDATA/UnicodeData.txt") |
|||
|
|||
codepoints = get_codepoints(file) |
|||
|
|||
CUTOFF=0x10000 |
|||
singletons0 = [] |
|||
singletons1 = [] |
|||
normal0 = [] |
|||
normal1 = [] |
|||
extra = [] |
|||
|
|||
for a, b in to_ranges(get_escaped(codepoints)): |
|||
if a > 2 * CUTOFF: |
|||
extra.append((a, b - a)) |
|||
elif a == b - 1: |
|||
if a & CUTOFF: |
|||
singletons1.append(a & ~CUTOFF) |
|||
else: |
|||
singletons0.append(a) |
|||
elif a == b - 2: |
|||
if a & CUTOFF: |
|||
singletons1.append(a & ~CUTOFF) |
|||
singletons1.append((a + 1) & ~CUTOFF) |
|||
else: |
|||
singletons0.append(a) |
|||
singletons0.append(a + 1) |
|||
else: |
|||
if a >= 2 * CUTOFF: |
|||
extra.append((a, b - a)) |
|||
elif a & CUTOFF: |
|||
normal1.append((a & ~CUTOFF, b - a)) |
|||
else: |
|||
normal0.append((a, b - a)) |
|||
|
|||
singletons0u, singletons0l = compress_singletons(singletons0) |
|||
singletons1u, singletons1l = compress_singletons(singletons1) |
|||
normal0 = compress_normal(normal0) |
|||
normal1 = compress_normal(normal1) |
|||
|
|||
print("""\ |
|||
FMT_FUNC auto is_printable(uint32_t cp) -> bool {\ |
|||
""") |
|||
print_singletons(singletons0u, singletons0l, 'singletons0', 'singletons0_lower') |
|||
print_singletons(singletons1u, singletons1l, 'singletons1', 'singletons1_lower') |
|||
print_normal(normal0, 'normal0') |
|||
print_normal(normal1, 'normal1') |
|||
print("""\ |
|||
auto lower = static_cast<uint16_t>(cp); |
|||
if (cp < 0x10000) { |
|||
return is_printable(lower, singletons0, |
|||
sizeof(singletons0) / sizeof(*singletons0), |
|||
singletons0_lower, normal0, sizeof(normal0)); |
|||
} |
|||
if (cp < 0x20000) { |
|||
return is_printable(lower, singletons1, |
|||
sizeof(singletons1) / sizeof(*singletons1), |
|||
singletons1_lower, normal1, sizeof(normal1)); |
|||
}\ |
|||
""") |
|||
for a, b in extra: |
|||
print(" if (0x{:x} <= cp && cp < 0x{:x}) return false;".format(a, a + b)) |
|||
print("""\ |
|||
return cp < 0x{:x}; |
|||
}}\ |
|||
""".format(NUM_CODEPOINTS)) |
|||
|
|||
if __name__ == '__main__': |
|||
main() |
@ -0,0 +1,317 @@ |
|||
# A basic mkdocstrings handler for {fmt}. |
|||
# Copyright (c) 2012 - present, Victor Zverovich |
|||
|
|||
import os |
|||
from pathlib import Path |
|||
from typing import Any, List, Mapping, Optional |
|||
from subprocess import CalledProcessError, PIPE, Popen, STDOUT |
|||
import xml.etree.ElementTree as et |
|||
|
|||
from mkdocstrings.handlers.base import BaseHandler |
|||
|
|||
class Definition: |
|||
'''A definition extracted by Doxygen.''' |
|||
def __init__(self, name: str, kind: Optional[str] = None, |
|||
node: Optional[et.Element] = None, |
|||
is_member: bool = False): |
|||
self.name = name |
|||
self.kind = kind if kind is not None else node.get('kind') |
|||
self.id = name if not is_member else None |
|||
self.params = None |
|||
self.members = None |
|||
|
|||
# A map from Doxygen to HTML tags. |
|||
tag_map = { |
|||
'bold': 'b', |
|||
'emphasis': 'em', |
|||
'computeroutput': 'code', |
|||
'para': 'p', |
|||
'programlisting': 'pre', |
|||
'verbatim': 'pre' |
|||
} |
|||
|
|||
# A map from Doxygen tags to text. |
|||
tag_text_map = { |
|||
'codeline': '', |
|||
'highlight': '', |
|||
'sp': ' ' |
|||
} |
|||
|
|||
def escape_html(s: str) -> str: |
|||
return s.replace("<", "<") |
|||
|
|||
def doxyxml2html(nodes: List[et.Element]): |
|||
out = '' |
|||
for n in nodes: |
|||
tag = tag_map.get(n.tag) |
|||
if not tag: |
|||
out += tag_text_map[n.tag] |
|||
out += '<' + tag + '>' if tag else '' |
|||
out += '<code class="language-cpp">' if tag == 'pre' else '' |
|||
if n.text: |
|||
out += escape_html(n.text) |
|||
out += doxyxml2html(n) |
|||
out += '</code>' if tag == 'pre' else '' |
|||
out += '</' + tag + '>' if tag else '' |
|||
if n.tail: |
|||
out += n.tail |
|||
return out |
|||
|
|||
def convert_template_params(node: et.Element) -> Optional[List[Definition]]: |
|||
templateparamlist = node.find('templateparamlist') |
|||
if templateparamlist is None: |
|||
return None |
|||
params = [] |
|||
for param_node in templateparamlist.findall('param'): |
|||
name = param_node.find('declname') |
|||
param = Definition(name.text if name is not None else '', 'param') |
|||
param.type = param_node.find('type').text |
|||
params.append(param) |
|||
return params |
|||
|
|||
def get_description(node: et.Element) -> List[et.Element]: |
|||
return node.findall('briefdescription/para') + \ |
|||
node.findall('detaileddescription/para') |
|||
|
|||
def normalize_type(type: str) -> str: |
|||
type = type.replace('< ', '<').replace(' >', '>') |
|||
return type.replace(' &', '&').replace(' *', '*') |
|||
|
|||
def convert_type(type: et.Element) -> str: |
|||
if type is None: |
|||
return None |
|||
result = type.text if type.text else '' |
|||
for ref in type: |
|||
result += ref.text |
|||
if ref.tail: |
|||
result += ref.tail |
|||
result += type.tail.strip() |
|||
return normalize_type(result) |
|||
|
|||
def convert_params(func: et.Element) -> Definition: |
|||
params = [] |
|||
for p in func.findall('param'): |
|||
d = Definition(p.find('declname').text, 'param') |
|||
d.type = convert_type(p.find('type')) |
|||
params.append(d) |
|||
return params |
|||
|
|||
def convert_return_type(d: Definition, node: et.Element) -> None: |
|||
d.trailing_return_type = None |
|||
if d.type == 'auto' or d.type == 'constexpr auto': |
|||
parts = node.find('argsstring').text.split(' -> ') |
|||
if len(parts) > 1: |
|||
d.trailing_return_type = normalize_type(parts[1]) |
|||
|
|||
def render_param(param: Definition) -> str: |
|||
return param.type + (f' {param.name}' if len(param.name) > 0 else '') |
|||
|
|||
def render_decl(d: Definition) -> None: |
|||
text = '' |
|||
if d.id is not None: |
|||
text += f'<a id="{d.id}">\n' |
|||
text += '<pre><code class="language-cpp decl">' |
|||
|
|||
text += '<div>' |
|||
if d.template_params is not None: |
|||
text += 'template <' |
|||
text += ', '.join([render_param(p) for p in d.template_params]) |
|||
text += '>\n' |
|||
text += '</div>' |
|||
|
|||
text += '<div>' |
|||
end = ';' |
|||
if d.kind == 'function' or d.kind == 'variable': |
|||
text += d.type + ' ' if len(d.type) > 0 else '' |
|||
elif d.kind == 'typedef': |
|||
text += 'using ' |
|||
elif d.kind == 'define': |
|||
end = '' |
|||
else: |
|||
text += d.kind + ' ' |
|||
text += d.name |
|||
|
|||
if d.params is not None: |
|||
params = ', '.join([ |
|||
(p.type + ' ' if p.type else '') + p.name for p in d.params]) |
|||
text += '(' + escape_html(params) + ')' |
|||
if d.trailing_return_type: |
|||
text += ' -⁠> ' + escape_html(d.trailing_return_type) |
|||
elif d.kind == 'typedef': |
|||
text += ' = ' + escape_html(d.type) |
|||
|
|||
text += end |
|||
text += '</div>' |
|||
text += '</code></pre>\n' |
|||
if d.id is not None: |
|||
text += f'</a>\n' |
|||
return text |
|||
|
|||
class CxxHandler(BaseHandler): |
|||
def __init__(self, **kwargs: Any) -> None: |
|||
super().__init__(handler='cxx', **kwargs) |
|||
|
|||
headers = [ |
|||
'args.h', 'base.h', 'chrono.h', 'color.h', 'compile.h', 'format.h', |
|||
'os.h', 'ostream.h', 'printf.h', 'ranges.h', 'std.h', 'xchar.h' |
|||
] |
|||
|
|||
# Run doxygen. |
|||
cmd = ['doxygen', '-'] |
|||
support_dir = Path(__file__).parents[3] |
|||
top_dir = os.path.dirname(support_dir) |
|||
include_dir = os.path.join(top_dir, 'include', 'fmt') |
|||
self._ns2doxyxml = {} |
|||
build_dir = os.path.join(top_dir, 'build') |
|||
os.makedirs(build_dir, exist_ok=True) |
|||
self._doxyxml_dir = os.path.join(build_dir, 'doxyxml') |
|||
p = Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=STDOUT) |
|||
_, _ = p.communicate(input=r''' |
|||
PROJECT_NAME = fmt |
|||
GENERATE_XML = YES |
|||
GENERATE_LATEX = NO |
|||
GENERATE_HTML = NO |
|||
INPUT = {0} |
|||
XML_OUTPUT = {1} |
|||
QUIET = YES |
|||
AUTOLINK_SUPPORT = NO |
|||
MACRO_EXPANSION = YES |
|||
PREDEFINED = _WIN32=1 \ |
|||
__linux__=1 \ |
|||
FMT_ENABLE_IF(...)= \ |
|||
FMT_USE_USER_DEFINED_LITERALS=1 \ |
|||
FMT_USE_ALIAS_TEMPLATES=1 \ |
|||
FMT_USE_NONTYPE_TEMPLATE_ARGS=1 \ |
|||
FMT_API= \ |
|||
"FMT_BEGIN_NAMESPACE=namespace fmt {{" \ |
|||
"FMT_END_NAMESPACE=}}" \ |
|||
"FMT_DOC=1" |
|||
'''.format( |
|||
' '.join([os.path.join(include_dir, h) for h in headers]), |
|||
self._doxyxml_dir).encode('utf-8')) |
|||
if p.returncode != 0: |
|||
raise CalledProcessError(p.returncode, cmd) |
|||
|
|||
# Merge all file-level XMLs into one to simplify search. |
|||
self._file_doxyxml = None |
|||
for h in headers: |
|||
filename = h.replace(".h", "_8h.xml") |
|||
with open(os.path.join(self._doxyxml_dir, filename)) as f: |
|||
doxyxml = et.parse(f) |
|||
if self._file_doxyxml is None: |
|||
self._file_doxyxml = doxyxml |
|||
continue |
|||
root = self._file_doxyxml.getroot() |
|||
for node in doxyxml.getroot(): |
|||
root.append(node) |
|||
|
|||
def collect_compound(self, identifier: str, |
|||
cls: List[et.Element]) -> Definition: |
|||
'''Collect a compound definition such as a struct.''' |
|||
path = os.path.join(self._doxyxml_dir, cls[0].get('refid') + '.xml') |
|||
with open(path) as f: |
|||
xml = et.parse(f) |
|||
node = xml.find('compounddef') |
|||
d = Definition(identifier, node=node) |
|||
d.template_params = convert_template_params(node) |
|||
d.desc = get_description(node) |
|||
d.members = [] |
|||
for m in node.findall('sectiondef[@kind="public-attrib"]/memberdef') + \ |
|||
node.findall('sectiondef[@kind="public-func"]/memberdef'): |
|||
name = m.find('name').text |
|||
# Doxygen incorrectly classifies members of private unnamed unions as |
|||
# public members of the containing class. |
|||
if name.endswith('_'): |
|||
continue |
|||
desc = get_description(m) |
|||
if len(desc) == 0: |
|||
continue |
|||
kind = m.get('kind') |
|||
member = Definition(name if name else '', kind=kind, is_member=True) |
|||
type = m.find('type').text |
|||
member.type = type if type else '' |
|||
if kind == 'function': |
|||
member.params = convert_params(m) |
|||
convert_return_type(member, m) |
|||
member.template_params = None |
|||
member.desc = desc |
|||
d.members.append(member) |
|||
return d |
|||
|
|||
def collect(self, identifier: str, config: Mapping[str, Any]) -> Definition: |
|||
qual_name = 'fmt::' + identifier |
|||
|
|||
param_str = None |
|||
paren = qual_name.find('(') |
|||
if paren > 0: |
|||
qual_name, param_str = qual_name[:paren], qual_name[paren + 1:-1] |
|||
|
|||
colons = qual_name.rfind('::') |
|||
namespace, name = qual_name[:colons], qual_name[colons + 2:] |
|||
|
|||
# Load XML. |
|||
doxyxml = self._ns2doxyxml.get(namespace) |
|||
if doxyxml is None: |
|||
path = f'namespace{namespace.replace("::", "_1_1")}.xml' |
|||
with open(os.path.join(self._doxyxml_dir, path)) as f: |
|||
doxyxml = et.parse(f) |
|||
self._ns2doxyxml[namespace] = doxyxml |
|||
|
|||
nodes = doxyxml.findall( |
|||
f"compounddef/sectiondef/memberdef/name[.='{name}']/..") |
|||
if len(nodes) == 0: |
|||
nodes = self._file_doxyxml.findall( |
|||
f"compounddef/sectiondef/memberdef/name[.='{name}']/..") |
|||
candidates = [] |
|||
for node in nodes: |
|||
# Process a function or a typedef. |
|||
params = None |
|||
d = Definition(name, node=node) |
|||
if d.kind == 'function': |
|||
params = convert_params(node) |
|||
node_param_str = ', '.join([p.type for p in params]) |
|||
if param_str and param_str != node_param_str: |
|||
candidates.append(f'{name}({node_param_str})') |
|||
continue |
|||
elif d.kind == 'define': |
|||
params = [] |
|||
for p in node.findall('param'): |
|||
param = Definition(p.find('defname').text, kind='param') |
|||
param.type = None |
|||
params.append(param) |
|||
d.type = convert_type(node.find('type')) |
|||
d.template_params = convert_template_params(node) |
|||
d.params = params |
|||
convert_return_type(d, node) |
|||
d.desc = get_description(node) |
|||
return d |
|||
|
|||
cls = doxyxml.findall(f"compounddef/innerclass[.='{qual_name}']") |
|||
if not cls: |
|||
raise Exception(f'Cannot find {identifier}. Candidates: {candidates}') |
|||
return self.collect_compound(identifier, cls) |
|||
|
|||
def render(self, d: Definition, config: dict) -> str: |
|||
if d.id is not None: |
|||
self.do_heading('', 0, id=d.id) |
|||
text = '<div class="docblock">\n' |
|||
text += render_decl(d) |
|||
text += '<div class="docblock-desc">\n' |
|||
text += doxyxml2html(d.desc) |
|||
if d.members is not None: |
|||
for m in d.members: |
|||
text += self.render(m, config) |
|||
text += '</div>\n' |
|||
text += '</div>\n' |
|||
return text |
|||
|
|||
def get_handler(theme: str, custom_templates: Optional[str] = None, |
|||
**config: Any) -> CxxHandler: |
|||
'''Return an instance of `CxxHandler`. |
|||
|
|||
Arguments: |
|||
theme: The theme to use when rendering contents. |
|||
custom_templates: Directory containing custom templates. |
|||
**config: Configuration passed to the handler. |
|||
''' |
|||
return CxxHandler(theme=theme, custom_templates=custom_templates) |
@ -0,0 +1 @@ |
|||
mkdocsstrings requires a handler to have a templates directory. |
Write
Preview
Loading…
Cancel
Save
Reference in new issue