From e4134f9c1f2ea025393e6bdea01df21d30aeb350 Mon Sep 17 00:00:00 2001 From: Kam Date: Tue, 7 Jul 2026 10:43:54 +1000 Subject: [PATCH] Fix nested group and global class output --- README.md | 7 +- docs/cli-reference.md | 2 + docs/grouping-modes.md | 4 +- docs/writing-doxygen.md | 2 +- example/doc/api-bicycle.md | 77 +- example/doc/api-mountainbike.md | 77 +- example/doc/api-racingbike.md | 77 +- example/doc/api.md | 55 + example/doc/page-changelog.md | 2 +- example/doc/page-overview.md | 4 +- example/src/transport.h | 4 +- example/xml/transport_8h.xml | 4 +- src/compound.ts | 15 +- src/helpers.ts | 7 +- src/index.ts | 228 ++- src/parser.ts | 16 + src/templates.ts | 12 + templates/cpp/index.md | 41 +- templates/cpp/namespace.md | 17 + templates/java/index.md | 41 +- templates/java/namespace.md | 23 +- test/fixtures/global-groups/Doxyfile | 14 + test/fixtures/global-groups/src/global.h | 55 + .../global-groups/xml-out/xml/Doxyfile.xml | 369 ++++ .../xml-out/xml/classglobal__class.xml | 32 + .../xml-out/xml/classnested__class.xml | 32 + .../global-groups/xml-out/xml/combine.xslt | 15 + .../global-groups/xml-out/xml/compound.xsd | 1787 +++++++++++++++++ .../dir_68267d1309a1af8e8297ef4c3efbcdba.xml | 12 + .../global-groups/xml-out/xml/doxyfile.xsd | 365 ++++ .../global-groups/xml-out/xml/global_8h.xml | 66 + .../xml-out/xml/group__global__group.xml | 27 + .../xml-out/xml/group__nested__group.xml | 13 + .../global-groups/xml-out/xml/index.xml | 21 + .../global-groups/xml-out/xml/index.xsd | 73 + .../global-groups/xml-out/xml/xml.xsd | 23 + test/integration.test.ts | 121 ++ 37 files changed, 3605 insertions(+), 135 deletions(-) create mode 100644 example/doc/api.md create mode 100644 test/fixtures/global-groups/Doxyfile create mode 100644 test/fixtures/global-groups/src/global.h create mode 100644 test/fixtures/global-groups/xml-out/xml/Doxyfile.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/classglobal__class.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/classnested__class.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/combine.xslt create mode 100644 test/fixtures/global-groups/xml-out/xml/compound.xsd create mode 100644 test/fixtures/global-groups/xml-out/xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/doxyfile.xsd create mode 100644 test/fixtures/global-groups/xml-out/xml/global_8h.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/group__global__group.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/group__nested__group.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/index.xml create mode 100644 test/fixtures/global-groups/xml-out/xml/index.xsd create mode 100644 test/fixtures/global-groups/xml-out/xml/xml.xsd diff --git a/README.md b/README.md index 3eb051ee..55f01432 100644 --- a/README.md +++ b/README.md @@ -67,8 +67,9 @@ Moxygen supports both of the common Doxygen documentation styles: 1. Explicit grouped compounds. - `@defgroup`, `@ingroup`, and related tags produce normal Doxygen group XML. - Moxygen renders those groups directly. + - The root `api.md` page lists top-level groups before classes, and nested group pages list their child groups with breadcrumbs. 2. File-level grouped code. - - Some codebases group files with `@addtogroup` / `@ingroup`, but Doxygen emits sparse group XML and leaves the real compound ownership in file XML. + - Some codebases group files with `@addtogroup` blocks or `@file` comments with `@ingroup`, but Doxygen emits sparse group XML and leaves the real compound ownership in file XML. - Moxygen can recover those grouped classes, namespaces, and members when you pass `--source-root`. - Shared umbrella namespaces across multiple groups are preserved without dropping group-owned root classes, and cross-page refs are resolved correctly for both markdown mirrors and generated multi-page output. @@ -95,6 +96,8 @@ Multi-file grouped output: moxygen --anchors --groups --output api-%s.md /path/to/doxygen/xml ``` +Grouped output writes group files such as `api-mygroup.md` plus `api.md` for root-level globals and the top-level group/class index, unless `--noindex` is set. + Grouped output recovered from file-level grouping comments: ``` moxygen --anchors --groups --source-root /path/to/project/src \ @@ -106,6 +109,8 @@ Per-class files: moxygen --classes --output api-%s.md /path/to/doxygen/xml ``` +Per-class output writes class files plus `api.md` for root-level globals and the class/namespace index, unless `--noindex` is set. + Java project: ``` moxygen --language java --anchors /path/to/doxygen/xml diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 19d6e745..2bc2dfdf 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -26,3 +26,5 @@ moxygen [options] | `-f, --frontmatter` | Prepend YAML frontmatter to output files | | `-L, --logfile [file]` | Output log messages to file | | `-q, --quiet` | Quiet mode | + +When `--groups` or `--classes` is enabled, Moxygen writes the split files and also writes `api.md` for root-level globals and the top-level index unless `--noindex` is set. diff --git a/docs/grouping-modes.md b/docs/grouping-modes.md index e6b54807..7f6b5c1f 100644 --- a/docs/grouping-modes.md +++ b/docs/grouping-modes.md @@ -15,9 +15,11 @@ Use `@defgroup`, `@ingroup`, and related tags. Doxygen produces normal group XML moxygen --groups --output api-%s.md /path/to/xml ``` +The root `api.md` page lists top-level groups before classes. Group pages list nested groups before classes and include breadcrumbs back to their parent groups. + ## File-level grouped code -Some codebases group files with `@addtogroup` / `@ingroup`, but Doxygen emits sparse group XML and leaves the real compound ownership in file XML. +Some codebases group files with `@addtogroup` blocks or `@file` comments with `@ingroup`, but Doxygen emits sparse group XML and leaves the real compound ownership in file XML. Moxygen recovers those grouped classes, namespaces, and members when you pass `--source-root`: diff --git a/docs/writing-doxygen.md b/docs/writing-doxygen.md index d0791335..4f92bb95 100644 --- a/docs/writing-doxygen.md +++ b/docs/writing-doxygen.md @@ -28,7 +28,7 @@ If you want private members in the generated Markdown, Doxygen must emit them fi EXTRACT_PRIVATE = YES ``` -For grouped output, keep the XML directory and source tree relationship clear. If Doxygen emits sparse group XML for file-level `@addtogroup` or `@ingroup` comments, pass `--source-root` so Moxygen can inspect the source comments that Doxygen left out of the group compound XML. +For grouped output, keep the XML directory and source tree relationship clear. If Doxygen emits sparse group XML for file-level `@addtogroup` blocks or `@file` comments with `@ingroup`, pass `--source-root` so Moxygen can inspect the source comments that Doxygen left out of the group compound XML. ```bash moxygen --groups \ diff --git a/example/doc/api-bicycle.md b/example/doc/api-bicycle.md index 74cf3697..351f029d 100644 --- a/example/doc/api-bicycle.md +++ b/example/doc/api-bicycle.md @@ -1,4 +1,6 @@ -# bicycle {#group__bicycle} +{#byciclemodule} + +# Bycicle module Bicycle module contains the bicycle class. Bicycles are a useful way of transporting oneself, without too much effort. @@ -6,54 +8,99 @@ Bicycle module contains the bicycle class. Bicycles are a useful way of transpor | Name | Description | |------|-------------| -| [`Bicycle`](#classtransport_1_1Bicycle) | Standard bicycle class. | +| [`Bicycle`](#bicycle) | Standard bicycle class. | + +{#bicycle} -## Bicycle {#classtransport_1_1Bicycle} +## Bicycle -> **Subclasses:** `transport::MountainBike`, `transport::RacingBike` -> **Defined in:** `bicycle.h` +```cpp +#include +``` + +```cpp +class Bicycle +``` + +Defined in src/bicycle.h:17 + +> **Subclassed by:** [`MountainBike`](api-mountainbike.md#mountainbike), [`RacingBike`](api-racingbike.md#racingbike) Standard bicycle class. -[Bicycle](#classtransport_1_1Bicycle) implements a standard bicycle. Bicycles are a useful way of transporting oneself, without too much effort (unless you go uphill or against the wind). If there are a lot of people on the road, you can use `RingBell` to ring your bell (**note**, not all bicycles have bells!). +[Bicycle](#bicycle) implements a standard bicycle. Bicycles are a useful way of transporting oneself, without too much effort (unless you go uphill or against the wind). If there are a lot of people on the road, you can use `RingBell` to ring your bell (**note**, not all bicycles have bells!). -### Members +### List of all members -| Name | Description | -|------|-------------| -| [`PedalHarder`](#classtransport_1_1Bicycle_1a7df6cce8f18012fb07bef5be9dadd8ef) | PedalHarder makes you go faster (usually). | -| [`RingBell`](#classtransport_1_1Bicycle_1a7d2be572f09c78b4d4ae38ef22f3e98b) | Ring bell on the bike. | -| [`~Bicycle`](#classtransport_1_1Bicycle_1a5f62d09b772a7705634bfb3551803c25) | Default destructor. | +| Name | Kind | Owner | +|------|------|-------| +| [`PedalHarder`](#pedalharder) | `function` | Declared here | +| [`RingBell`](#ringbell) | `function` | Declared here | +| [`~Bicycle`](#bicycle-1) | `function` | Declared here | + +### Public Methods + +| Return | Name | Description | +|--------|------|-------------| +| `void` | [`PedalHarder`](#pedalharder) `virtual` | PedalHarder makes you go faster (usually). | +| `void` | [`RingBell`](#ringbell) `virtual` | Ring bell on the bike. | +| | [`~Bicycle`](#bicycle-1) `virtual` | Default destructor. | --- -#### PedalHarder {#classtransport_1_1Bicycle_1a7df6cce8f18012fb07bef5be9dadd8ef} +{#pedalharder} + +#### PedalHarder + +`virtual` ```cpp virtual void PedalHarder() ``` +Defined in src/bicycle.h:20 + PedalHarder makes you go faster (usually). +##### Reimplemented by + +- [`PedalHarder`](api-racingbike.md#pedalharder-1) + --- -#### RingBell {#classtransport_1_1Bicycle_1a7d2be572f09c78b4d4ae38ef22f3e98b} +{#ringbell} + +#### RingBell + +`virtual` ```cpp virtual void RingBell() ``` +Defined in src/bicycle.h:27 + Ring bell on the bike. RingBell rings the bell on the bike. Note that not all bikes have bells. +##### Reimplemented by + +- [`RingBell`](api-racingbike.md#ringbell-1) + --- -#### ~Bicycle {#classtransport_1_1Bicycle_1a5f62d09b772a7705634bfb3551803c25} +{#bicycle-1} + +#### ~Bicycle + +`virtual` ```cpp virtual ~Bicycle() ``` +Defined in src/bicycle.h:30 + Default destructor. diff --git a/example/doc/api-mountainbike.md b/example/doc/api-mountainbike.md index 562bf010..fbedd61f 100644 --- a/example/doc/api-mountainbike.md +++ b/example/doc/api-mountainbike.md @@ -1,4 +1,6 @@ -# mountainbike {#group__mountainbike} +{#mountainbikemodule} + +# Mountain bike module Mountain bike module contains the `MountainBike` class. Mountain bikes are a kind of bike for cycling on rough terrain. @@ -6,32 +8,65 @@ Mountain bike module contains the `MountainBike` class. Mountain bikes are a kin | Name | Description | |------|-------------| -| [`MountainBike`](#classtransport_1_1MountainBike) | Mountain bike implementation of a `[Bicycle](example/doc/api-bicycle.md#classtransport_1_1Bicycle)`. | +| [`MountainBike`](#mountainbike) | Mountain bike implementation of a `[Bicycle](api-bicycle.md#bicycle)`. | -## MountainBike {#classtransport_1_1MountainBike} +{#mountainbike} -> **Extends:** `transport::Bicycle` -> **Defined in:** `mountainbike.h` +## MountainBike -Mountain bike implementation of a `[Bicycle](example/doc/api-bicycle.md#classtransport_1_1Bicycle)`. +```cpp +#include +``` -[MountainBike](#classtransport_1_1MountainBike) is an implementation of a [Bicycle](example/doc/api-bicycle.md#classtransport_1_1Bicycle) providing a bike for cycling on rough terrain. Mountain bikes are pretty cool because they have stuff like **Suspension** (and you can even adjust it using SetSuspension). If you're looking for a bike for use on the road, you might be better off using a [RacingBike](example/doc/api-racingbike.md#classtransport_1_1RacingBike) though. +```cpp +class MountainBike +``` -### Members +Defined in src/mountainbike.h:20 -| Name | Description | -|------|-------------| -| [`SetSuspension`](#classtransport_1_1MountainBike_1a04caecd7e5ff7572b6ac1dc283510301) | Set suspension stiffness. the suspension stiffness. | -| [`ChangeBreak`](#classtransport_1_1MountainBike_1afd02513876a196e98acaacdc555aeb52) | Change the break type. the break type. the type of the break. | +> **Inherits:** [`Bicycle`](api-bicycle.md#bicycle) + +Mountain bike implementation of a `[Bicycle](api-bicycle.md#bicycle)`. + +[MountainBike](#mountainbike) is an implementation of a [Bicycle](api-bicycle.md#bicycle) providing a bike for cycling on rough terrain. Mountain bikes are pretty cool because they have stuff like **Suspension** (and you can even adjust it using SetSuspension). If you're looking for a bike for use on the road, you might be better off using a [RacingBike](api-racingbike.md#racingbike) though. + +### List of all members + +| Name | Kind | Owner | +|------|------|-------| +| [`SetSuspension`](#setsuspension) | `function` | Declared here | +| [`ChangeBreak`](#changebreak) | `function` | Declared here | +| [`PedalHarder`](api-bicycle.md#pedalharder) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | +| [`RingBell`](api-bicycle.md#ringbell) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | +| [`~Bicycle`](api-bicycle.md#bicycle-1) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | + +### Inherited from [`Bicycle`](api-bicycle.md#bicycle) + +| Kind | Name | Description | +|------|------|-------------| +| `function` | [`PedalHarder`](api-bicycle.md#pedalharder) `virtual` | PedalHarder makes you go faster (usually). | +| `function` | [`RingBell`](api-bicycle.md#ringbell) `virtual` | Ring bell on the bike. | +| `function` | [`~Bicycle`](api-bicycle.md#bicycle-1) `virtual` | Default destructor. | + +### Public Methods + +| Return | Name | Description | +|--------|------|-------------| +| `bool` | [`SetSuspension`](#setsuspension) | Set suspension stiffness. the suspension stiffness. | +| `bool` | [`ChangeBreak`](#changebreak) `inline` | Change the break type. the break type. the type of the break. | --- -#### SetSuspension {#classtransport_1_1MountainBike_1a04caecd7e5ff7572b6ac1dc283510301} +{#setsuspension} + +#### SetSuspension ```cpp bool SetSuspension(double stiffness) ``` +Defined in src/mountainbike.h:32 + Set suspension stiffness. the suspension stiffness. SetSuspension changes the stiffness of the suspension on the bike. The method will return false if the stiffness could not be adjusted. @@ -39,18 +74,20 @@ SetSuspension changes the stiffness of the suspension on the bike. The method wi #### Returns true if the suspension was adjusted successfully, false otherwise. -| Parameter | Type | Description | -|-----------|------|-------------| -| `stiffness` | `double` | | - --- -#### ChangeBreak {#classtransport_1_1MountainBike_1afd02513876a196e98acaacdc555aeb52} +{#changebreak} + +#### ChangeBreak + +`inline` ```cpp template inline bool ChangeBreak(BreakType breakType) ``` +Defined in src/mountainbike.h:46 + Change the break type. the break type. the type of the break. ChangesBreak changes the type of break fitted to the bike. The method will return false if the break type could not be fitted. @@ -58,7 +95,3 @@ ChangesBreak changes the type of break fitted to the bike. The method will retur #### Returns true if the break was adjusted successfully. false otherise -| Parameter | Type | Description | -|-----------|------|-------------| -| `breakType` | `BreakType` | | - diff --git a/example/doc/api-racingbike.md b/example/doc/api-racingbike.md index 16326baf..fc0d5aa1 100644 --- a/example/doc/api-racingbike.md +++ b/example/doc/api-racingbike.md @@ -1,4 +1,6 @@ -# racingbike {#group__racingbike} +{#racingbikemodule} + +# Racing bike module Racing bike module contains the `RacingBike` class. Racing bikes are a special kind of bike which can go much faster on the road, with much less effort. @@ -6,43 +8,92 @@ Racing bike module contains the `RacingBike` class. Racing bikes are a special k | Name | Description | |------|-------------| -| [`RacingBike`](#classtransport_1_1RacingBike) | Racing bike class. | +| [`RacingBike`](#racingbike) | Racing bike class. | + +{#racingbike} -## RacingBike {#classtransport_1_1RacingBike} +## RacingBike -> **Extends:** `transport::Bicycle` -> **Defined in:** `racingbike.h` +```cpp +#include +``` + +```cpp +class RacingBike +``` + +Defined in src/racingbike.h:17 + +> **Inherits:** [`Bicycle`](api-bicycle.md#bicycle) Racing bike class. -[RacingBike](#classtransport_1_1RacingBike) is a special kind of bike which can go much faster on the road, with much less effort (even uphill!). It doesn't make sense to call `RingBell` on a racing bike for they don't have bells. +[RacingBike](#racingbike) is a special kind of bike which can go much faster on the road, with much less effort (even uphill!). It doesn't make sense to call `RingBell` on a racing bike for they don't have bells. -### Members +### List of all members -| Name | Description | -|------|-------------| -| [`PedalHarder`](#classtransport_1_1RacingBike_1ab557c5727daa07a5001782d5dcd46c5b) | PedalHarder makes you go faster (usually). | -| [`RingBell`](#classtransport_1_1RacingBike_1ad32dc3b06a453fba3e20329842bb318b) | Ring bell on the bike. | +| Name | Kind | Owner | +|------|------|-------| +| [`PedalHarder`](#pedalharder-1) | `function` | Declared here | +| [`RingBell`](#ringbell-1) | `function` | Declared here | +| [`PedalHarder`](api-bicycle.md#pedalharder) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | +| [`RingBell`](api-bicycle.md#ringbell) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | +| [`~Bicycle`](api-bicycle.md#bicycle-1) | `function` | Inherited from [`Bicycle`](api-bicycle.md#bicycle) | + +### Inherited from [`Bicycle`](api-bicycle.md#bicycle) + +| Kind | Name | Description | +|------|------|-------------| +| `function` | [`PedalHarder`](api-bicycle.md#pedalharder) `virtual` | PedalHarder makes you go faster (usually). | +| `function` | [`RingBell`](api-bicycle.md#ringbell) `virtual` | Ring bell on the bike. | +| `function` | [`~Bicycle`](api-bicycle.md#bicycle-1) `virtual` | Default destructor. | + +### Public Methods + +| Return | Name | Description | +|--------|------|-------------| +| `void` | [`PedalHarder`](#pedalharder-1) `virtual` | PedalHarder makes you go faster (usually). | +| `void` | [`RingBell`](#ringbell-1) `virtual` | Ring bell on the bike. | --- -#### PedalHarder {#classtransport_1_1RacingBike_1ab557c5727daa07a5001782d5dcd46c5b} +{#pedalharder-1} + +#### PedalHarder + +`virtual` ```cpp virtual void PedalHarder() ``` +Defined in src/racingbike.h:20 + PedalHarder makes you go faster (usually). +##### Reimplements + +- [`PedalHarder`](api-bicycle.md#pedalharder) + --- -#### RingBell {#classtransport_1_1RacingBike_1ad32dc3b06a453fba3e20329842bb318b} +{#ringbell-1} + +#### RingBell + +`virtual` ```cpp virtual void RingBell() ``` +Defined in src/racingbike.h:23 + Ring bell on the bike. RingBell rings the bell on the bike. Note that not all bikes have bells. +##### Reimplements + +- [`RingBell`](api-bicycle.md#ringbell) + diff --git a/example/doc/api.md b/example/doc/api.md new file mode 100644 index 00000000..70d2cf6a --- /dev/null +++ b/example/doc/api.md @@ -0,0 +1,55 @@ +# API Reference + +### Groups + +| Name | Description | +|------|-------------| +| [`Bycicle module`](api-bicycle.md#byciclemodule) | Bicycle module contains the bicycle class. Bicycles are a useful way of transporting oneself, without too much effort. | +| [`Racing bike module`](api-racingbike.md#racingbikemodule) | Racing bike module contains the `RacingBike` class. Racing bikes are a special kind of bike which can go much faster on the road, with much less effort. | +| [`Mountain bike module`](api-mountainbike.md#mountainbikemodule) | Mountain bike module contains the `MountainBike` class. Mountain bikes are a kind of bike for cycling on rough terrain. | + +### Namespaces + +| Name | Description | +|------|-------------| +| [`transport`](#transport-1) | | + +## Macros + +{#pedal_power_modifier} + +#### PEDAL_POWER_MODIFIER + +```cpp +PEDAL_POWER_MODIFIER() +``` + +The modifier value if pedal power is used. + +This definition exists in the default namespace and is ungrouped. It is displayed on the root API page when grouped output is used. + +## Enumerations + +{#transporttype} + +#### TransportType + +```cpp +enum TransportType +``` + +| Value | Description | +|-------|-------------| +| `Bycicle` | Bycicle type | +| `RacingBike` | Racing bike type | +| `RacingBike` | Mountain bike type | + +Enum class for transport types. + +This definition exists in the default namespace and is ungrouped. It is displayed on the root API page when grouped output is used. + +{#transport-1} + +# transport + +Generated by [Moxygen](https://0state.com/moxygen) \ No newline at end of file diff --git a/example/doc/page-changelog.md b/example/doc/page-changelog.md index 48ea8635..90c85392 100644 --- a/example/doc/page-changelog.md +++ b/example/doc/page-changelog.md @@ -1,4 +1,4 @@ -# changelog {#changelog} +# Changelog {#changelog} ## Version 1.0.1 diff --git a/example/doc/page-overview.md b/example/doc/page-overview.md index d1fa8900..2680c427 100644 --- a/example/doc/page-overview.md +++ b/example/doc/page-overview.md @@ -1,6 +1,6 @@ -# overview {#overview} +# Transport {#transport} -The `transport` namespace provides several differnt types of bycicles, including [transport::Bicycle](example/doc/api-bicycle.md#classtransport_1_1Bicycle), [transport::MountainBike](example/doc/api-mountainbike.md#classtransport_1_1MountainBike) (designed for rough terrain), and [transport::RacingBike](example/doc/api-racingbike.md#classtransport_1_1RacingBike) (designed specifically for challenging races). +The `transport` namespace provides several differnt types of bycicles, including [transport::Bicycle](api-bicycle.md#bicycle), [transport::MountainBike](api-mountainbike.md#mountainbike) (designed for rough terrain), and [transport::RacingBike](api-racingbike.md#racingbike) (designed specifically for challenging races). ## Changelog diff --git a/example/src/transport.h b/example/src/transport.h index 4e704aa2..d42de793 100644 --- a/example/src/transport.h +++ b/example/src/transport.h @@ -30,7 +30,7 @@ * Enum class for transport types. * * This definition exists in the default namespace and is ungrouped. - * It will *not* be displayed if the `groups` options is used. + * It is displayed on the root API page when grouped output is used. */ enum class TransportType { Bycicle, /*!< Bycicle type */ @@ -42,7 +42,7 @@ enum class TransportType { * The modifier value if pedal power is used. * * This definition exists in the default namespace and is ungrouped. - * It will *not* be displayed if the `groups` options is used. + * It is displayed on the root API page when grouped output is used. */ #define PEDAL_POWER_MODIFIER 9000 diff --git a/example/xml/transport_8h.xml b/example/xml/transport_8h.xml index e65a5ba0..2c690413 100644 --- a/example/xml/transport_8h.xml +++ b/example/xml/transport_8h.xml @@ -33,7 +33,7 @@ -The modifier value if pedal power is used.This definition exists in the default namespace and is ungrouped. It will not be displayed if the groups options is used. +The modifier value if pedal power is used.This definition exists in the default namespace and is ungrouped. It is displayed on the root API page when grouped output is used. @@ -67,7 +67,7 @@ -Enum class for transport types.This definition exists in the default namespace and is ungrouped. It will not be displayed if the groups options is used. +Enum class for transport types.This definition exists in the default namespace and is ungrouped. It is displayed on the root API page when grouped output is used. diff --git a/src/compound.ts b/src/compound.ts index bb1cf845..d9657d25 100644 --- a/src/compound.ts +++ b/src/compound.ts @@ -120,9 +120,18 @@ export function filterCollection( } } - // Skip items not belonging to current group - if (groupid && (item as Member).groupid !== groupid) { - continue; + // Skip items not belonging to current group. Nested groups own their + // own groupid, but still belong in the parent group's topic list. + if (groupid) { + if ('filtered' in item) { + const compound = item as Compound; + const parent = compound.parent as Compound | null; + if (compound.groupid !== groupid && !(compound.kind === 'group' && parent?.id === groupid)) { + continue; + } + } else if ((item as Member).groupid !== groupid) { + continue; + } } const categoryKey = (item as Record)[key] as string; diff --git a/src/helpers.ts b/src/helpers.ts index 80233efd..3e26e7bb 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -250,10 +250,10 @@ export function compoundPath(compound: Compound, options: MoxygenOptions): strin if (compound.kind === 'page') { return `${dirname(options.output)}/page-${compound.name}.md`; } - if (compound.kind === 'index' && options.groups) { + if (compound.kind === 'index' && (options.groups || options.classes)) { return `${dirname(options.output)}/api.md`; } - if (options.groups) { + if (options.groups && compound.kind === 'group') { return utilFormat(options.output, compound.groupname); } if (options.classes) { @@ -262,6 +262,9 @@ export function compoundPath(compound: Compound, options: MoxygenOptions): strin safePathSegment(compound.name), ); } + if (options.groups) { + return utilFormat(options.output, compound.groupname); + } return options.output; } diff --git a/src/index.ts b/src/index.ts index c3d7dc57..765e980d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -283,7 +283,34 @@ function attachRelationshipSummaries(compounds: Compound[], references: Referenc } } -const GROUP_MARKER_RE = /(?:@|\\)(?:addtogroup|ingroup)\s+([A-Za-z_][\w:-]*)/g; +const DOXYGEN_COMMENT_RE = /\/\*\*[\s\S]*?\*\/|\/\*![\s\S]*?\*\/|(?:^[ \t]*(?:\/\/\/|\/\/!).*(?:\r?\n|$))+/gm; +const ADDTOGROUP_MARKER_RE = /(?:@|\\)addtogroup\s+([A-Za-z_][\w:-]*)/g; +const INGROUP_MARKER_RE = /(?:@|\\)ingroup\s+([A-Za-z_][\w:-]*)/g; +const FILE_MARKER_RE = /(?:@|\\)file(?:\s|$)/; + +function addGroupMarkerTags(source: string, marker: RegExp, tags: Set): void { + marker.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = marker.exec(source)) !== null) { + tags.add(match[1]); + } +} + +function collectSourceGroupTags(source: string): string[] { + const tags = new Set(); + DOXYGEN_COMMENT_RE.lastIndex = 0; + + let comment: RegExpExecArray | null; + while ((comment = DOXYGEN_COMMENT_RE.exec(source)) !== null) { + const text = comment[0]; + addGroupMarkerTags(text, ADDTOGROUP_MARKER_RE, tags); + if (FILE_MARKER_RE.test(text)) { + addGroupMarkerTags(text, INGROUP_MARKER_RE, tags); + } + } + + return [...tags]; +} function resolveSourcePath(location: string, options: MoxygenOptions): string | undefined { const candidates: string[] = []; @@ -335,13 +362,7 @@ function readFileGroupTags(file: Compound, options: MoxygenOptions): string[] { } const source = readFileSync(sourcePath, 'utf8'); - const tags = new Set(); - let match: RegExpExecArray | null; - while ((match = GROUP_MARKER_RE.exec(source)) !== null) { - tags.add(match[1]); - } - - file.fileGroupTags = [...tags]; + file.fileGroupTags = collectSourceGroupTags(source); return file.fileGroupTags as string[]; } @@ -574,17 +595,22 @@ export async function generate( const seenPrep = new Set(); augmentGroupsFromFiles(root, groups, opts); finalizeGroups(groups, collectSharedNamespaceRefs(toArray(root, 'compounds', 'file') as Compound[], opts)); + const filters = groupAwareFilters(opts.filters); // Group-based: each @defgroup becomes a module for (const group of groups) { - filterChildren(group, opts.filters, group.id); + filterChildren(group, filters, group.id); prepareCompound(group); allCompounds.push(group); seenPrep.add(group.refid); for (const child of toFilteredArray(group, 'compounds')) { if (isJunkCompound(child)) continue; - filterChildren(child, opts.filters); + if (child.kind === 'group') { + filterChildren(child, filters, child.id); + } else { + filterChildren(child, opts.filters); + } prepareCompound(child); allCompounds.push(child); seenPrep.add(child.refid); @@ -631,7 +657,7 @@ export async function generate( for (const c of allCompounds) { slugMap.set(c.refid, slugify(c.name)); } - const pagePathMap = useGroups ? buildGroupedNamespacePagePathMap(groups) : undefined; + const pagePathMap = useGroups ? buildGroupedPagePathMap(groups) : undefined; attachRelationshipSummaries(allCompounds, references); // Second pass: render (dedup by refid) @@ -778,11 +804,13 @@ function lastSegment(ns: string): string { return parts[parts.length - 1] || ns; } -function buildGroupedNamespacePagePathMap(groups: Compound[]): PagePathMap { +function buildGroupedPagePathMap(groups: Compound[]): PagePathMap { const map: PagePathMap = new Map(); for (const group of groups) { const groupPath = `${slugify(group.name)}.html`; + map.set(group.refid, groupPath); + const namespaces = toArray(group, 'compounds', 'namespace') as Compound[]; for (const namespace of namespaces) { if (isJunkCompound(namespace)) continue; @@ -875,6 +903,61 @@ function searchCategoryForKind(kind: string): string { // run() — CLI API writing to disk // --------------------------------------------------------------------------- +const CLASS_OUTPUT_KINDS = new Set(['namespace', 'class', 'struct', 'union', 'interface', 'enum', 'concept']); + +function groupAwareFilters(filters: Filters): Filters { + return { + ...filters, + compounds: [...new Set(['group', ...filters.compounds])], + }; +} + +function isClassOutputCompound(compound: Compound): boolean { + return CLASS_OUTPUT_KINDS.has(compound.kind) && !isJunkCompound(compound); +} + +function uniqueCompounds(compounds: Compound[]): Compound[] { + const seen = new Set(); + const result: Compound[] = []; + for (const compound of compounds) { + if (seen.has(compound.refid)) continue; + seen.add(compound.refid); + result.push(compound); + } + return result; +} + +function collectClassOutputCompounds(root: Compound): Compound[] { + return uniqueCompounds( + (toArray(root, 'compounds') as Compound[]).filter(isClassOutputCompound), + ); +} + +function collectRootIndexCompounds(root: Compound, groups: Compound[]): Compound[] { + const topLevelGroups = groups.filter((group) => group.parent?.kind !== 'group'); + const rootCompounds = Object.values(root.compounds) + .filter((compound) => isClassOutputCompound(compound) && !compound.groupid); + return uniqueCompounds([...topLevelGroups, ...rootCompounds]); +} + +function prepareRootIndex(root: Compound, groups: Compound[], opts: MoxygenOptions): void { + root.filtered.members = filterNoise( + filterCollection(root.members, 'section', opts.filters.members) as Member[], + ); + root.filtered.compounds = collectRootIndexCompounds(root, groups); + root.filtered.sections = groupMembersBySection(root); +} + +function ensurePathMap(pagePathMap: PagePathMap | undefined): PagePathMap { + return pagePathMap ?? new Map(); +} + +function addCompoundPath(pagePathMap: PagePathMap, compound: Compound, opts: MoxygenOptions): void { + if (!pagePathMap.has(compound.refid)) { + pagePathMap.set(compound.refid, compoundPath(compound, opts)); + } +} + /** * Parse Doxygen XML and render Markdown output to disk. */ @@ -882,74 +965,89 @@ export async function run(options: Partial & { directory: string const opts = resolveOptions(options); const { root, references } = await loadAndPrepare(opts); let pagePathMap: PagePathMap | undefined; + const splitOutput = opts.groups || opts.classes; // --- Pass 1: filter + prepare all compounds --- const allCompounds: Compound[] = []; let writeRootIndex = false; + let rootBodyCompounds: Compound[] = []; + let groups: Compound[] = []; + let classOutputCompounds: Compound[] = []; if (opts.groups) { - const groups = toArray(root, 'compounds', 'group') as Compound[]; + groups = (toArray(root, 'compounds', 'group') as Compound[]) + .filter((group) => !isJunkCompound(group)); if (!groups.length) { throw new Error('You have enabled `groups` output, but no groups were located in your doxygen XML files.'); } augmentGroupsFromFiles(root, groups, opts); finalizeGroups(groups, collectSharedNamespaceRefs(toArray(root, 'compounds', 'file') as Compound[], opts)); + } + + if (opts.classes) { + classOutputCompounds = collectClassOutputCompounds(root); + if (!classOutputCompounds.length) { + throw new Error('You have enabled `classes` output, but no classes were located in your doxygen XML files.'); + } + } + + if (opts.groups) { + const filters = groupAwareFilters(opts.filters); for (const group of groups) { - filterChildren(group, opts.filters, group.id); + filterChildren(group, filters, group.id); prepareCompound(group); const children = toFilteredArray(group, 'compounds'); for (const c of children) prepareCompound(c); - if (!pagePathMap) { - pagePathMap = new Map(); - } - const pagePath = compoundPath(group, opts); - if (!pagePathMap.has(group.refid)) { - pagePathMap.set(group.refid, pagePath); - } + pagePathMap = ensurePathMap(pagePathMap); + addCompoundPath(pagePathMap, group, opts); for (const child of children) { - if (!pagePathMap.has(child.refid)) { - pagePathMap.set(child.refid, pagePath); + if (child.kind === 'group') { + addCompoundPath(pagePathMap, child, opts); + } else if (!opts.classes) { + pagePathMap.set(child.refid, compoundPath(group, opts)); } } for (const refid of (group.fileScopedNamespaceRefs as string[] | undefined) ?? []) { if (!pagePathMap.has(refid)) { - pagePathMap.set(refid, pagePath); + pagePathMap.set(refid, compoundPath(group, opts)); } } allCompounds.push(group, ...children); } + } + + if (opts.classes) { + for (const comp of classOutputCompounds) { + filterChildren(comp, opts.filters); + prepareCompound(comp); + allCompounds.push(comp); + pagePathMap = ensurePathMap(pagePathMap); + addCompoundPath(pagePathMap, comp, opts); + } + } - root.filtered.members = filterNoise( - filterCollection(root.members, 'section', opts.filters.members) as Member[], + if (splitOutput) { + prepareRootIndex(root, groups, opts); + rootBodyCompounds = opts.classes + ? [] + : root.filtered.compounds.filter((compound) => compound.kind !== 'group'); + writeRootIndex = !opts.noindex && ( + root.filtered.members.length > 0 || + root.filtered.compounds.length > 0 ); - root.filtered.compounds = []; - root.filtered.sections = groupMembersBySection(root); - writeRootIndex = !opts.noindex && root.filtered.members.length > 0; if (writeRootIndex) { - if (!pagePathMap) { - pagePathMap = new Map(); - } + pagePathMap = ensurePathMap(pagePathMap); const pagePath = compoundPath(root, opts); pagePathMap.set(root.refid, pagePath); for (const member of root.filtered.members) { pagePathMap.set(member.refid, pagePath); } - allCompounds.push(root); - } - } else if (opts.classes) { - const rootCompounds = toArray(root, 'compounds', 'namespace') as Compound[]; - if (!rootCompounds.length) { - throw new Error('You have enabled `classes` output, but no classes were located in your doxygen XML files.'); - } - for (const comp of rootCompounds) { - filterChildren(comp, opts.filters); - prepareCompound(comp); - allCompounds.push(comp); - for (const e of toFilteredArray(comp)) { - filterChildren(e, opts.filters); - prepareCompound(e); - allCompounds.push(e); + if (!opts.classes) { + for (const compound of rootBodyCompounds) { + pagePathMap.set(compound.refid, pagePath); + } } + allCompounds.push(root); } } else { filterChildren(root, opts.filters); @@ -965,25 +1063,25 @@ export async function run(options: Partial & { directory: string attachRelationshipSummaries(allCompounds, references); // --- Pass 2: render + write --- - if (opts.groups) { + if (splitOutput) { if (writeRootIndex) { - const contents = [templates.render(root)]; + const contents = templates.renderArray([root, ...rootBodyCompounds]); contents.push('Generated by [Moxygen](https://0state.com/moxygen)'); writeWithOptionalFrontmatter(root, contents, references, opts, anchorMap, pagePathMap); } - const groups = toArray(root, 'compounds', 'group') as Compound[]; - for (const group of groups) { - const compounds = toFilteredArray(group, 'compounds'); - compounds.unshift(group); - writeWithOptionalFrontmatter(group, templates.renderArray(compounds), references, opts, anchorMap, pagePathMap); + if (opts.groups) { + for (const group of groups) { + const compounds = toFilteredArray(group, 'compounds') + .filter((compound) => compound.kind !== 'group' && !opts.classes && compound.groupid === group.id); + compounds.unshift(group); + writeWithOptionalFrontmatter(group, templates.renderArray(compounds), references, opts, anchorMap, pagePathMap); + } } - } else if (opts.classes) { - const rootCompounds = toArray(root, 'compounds', 'namespace') as Compound[]; - for (const comp of rootCompounds) { - writeWithOptionalFrontmatter(comp, [templates.render(comp)], references, opts, anchorMap); - for (const e of toFilteredArray(comp)) { - writeWithOptionalFrontmatter(e, [templates.render(e)], references, opts, anchorMap); + + if (opts.classes) { + for (const comp of classOutputCompounds) { + writeWithOptionalFrontmatter(comp, [templates.render(comp)], references, opts, anchorMap, pagePathMap); } } } else { @@ -992,14 +1090,16 @@ export async function run(options: Partial & { directory: string const groups = (toArray(root, 'compounds', 'group') as Compound[]) .filter((g) => !isJunkCompound(g)); if (groups.length) { - augmentGroupsFromFiles(root, groups, opts); // to prepare + augmentGroupsFromFiles(root, groups, opts); finalizeGroups(groups, collectSharedNamespaceRefs(toArray(root, 'compounds', 'file') as Compound[], opts)); + const filters = groupAwareFilters(opts.filters); for (const group of groups) { - filterChildren(group, opts.filters, group.id); + filterChildren(group, filters, group.id); prepareCompound(group); - const childCompounds = toFilteredArray(group, 'compounds'); + const childCompounds = toFilteredArray(group, 'compounds') + .filter((compound) => compound.kind !== 'group' && compound.groupid === group.id); for (const c of childCompounds) prepareCompound(c); - compounds.push(group, ...childCompounds); // or insert at top or structured + compounds.push(group, ...childCompounds); } } } diff --git a/src/parser.ts b/src/parser.ts index 320c3528..7bf2331a 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -797,6 +797,14 @@ function assignClassToGroup(compound: Compound, child: Compound): void { assignCompoundGroup(child, compound); } +function assignGroupToGroup(compound: Compound, child: Compound): void { + if (child.parent) { + delete (child.parent as Compound).compounds[child.id]; + } + compound.compounds[child.id] = child; + child.parent = compound; +} + function extractPageSections(page: Compound, elements: XmlElement[]): void { for (const element of elements) { if (element['#name'] === 'sect1' || element['#name'] === 'sect2' || element['#name'] === 'sect3') { @@ -1016,6 +1024,14 @@ function parseCompound(compound: Compound, compounddef: Record) } } + if (compound.kind === 'group' && compounddef.innergroup) { + for (const groupdef of compounddef.innergroup as Array>) { + const groupAttrs = groupdef.$ as Record; + const ref = references[groupAttrs.refid] as Compound; + if (ref) assignGroupToGroup(compound, ref); + } + } + if (compound.kind === 'group') { pruneGroupTopLevelDuplicates(compound); } diff --git a/src/templates.ts b/src/templates.ts index 5309a00b..a2d12fcc 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -328,6 +328,18 @@ export function registerHelpers(options: Pick { + const breadcrumbs: Compound[] = []; + let current = compound.parent as Compound | null; + while (current) { + if (current.kind === 'group') { + breadcrumbs.unshift(current); + } + current = current.parent as Compound | null; + } + return breadcrumbs; + }); + // Not helper for conditionals Handlebars.registerHelper('not', (value: unknown) => !value); diff --git a/templates/cpp/index.md b/templates/cpp/index.md index 837684da..adb09bcf 100644 --- a/templates/cpp/index.md +++ b/templates/cpp/index.md @@ -1,11 +1,48 @@ # API Reference -{{#if filtered.compounds}} +{{#with (compoundsOfKind filtered.compounds "group") as |groups|}} +{{#if groups}} +### Groups + +| Name | Description | +|------|-------------| +{{#each groups}}| {{linkedName shortname refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "namespace") as |namespaces|}} +{{#if namespaces}} +### Namespaces + +| Name | Description | +|------|-------------| +{{#each namespaces}}| {{linkedName name refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "class" "struct" "interface") as |types|}} +{{#if types}} +### Classes + +| Name | Description | +|------|-------------| +{{#each types}}| {{linkedName name refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "enum") as |enums|}} +{{#if enums}} +### Enumerations + | Name | Description | |------|-------------| -{{#each filtered.compounds}}| [`{{shortname name}}`](#{{cleanId refid name}}) | {{cell summary}} | +{{#each enums}}| {{linkedName name refid}} | {{cell summary}} | {{/each}} {{/if}} +{{/with}} {{#each filtered.sections}} ## {{label}} diff --git a/templates/cpp/namespace.md b/templates/cpp/namespace.md index 461a0177..623f75c2 100644 --- a/templates/cpp/namespace.md +++ b/templates/cpp/namespace.md @@ -3,6 +3,12 @@ {{#if (eq kind "group")}} # {{#if this.shortname}}{{this.shortname}}{{else}}{{shortname name}}{{/if}} +{{#with (groupBreadcrumbs this) as |breadcrumbs|}} +{{#if breadcrumbs}} +> {{#each breadcrumbs}}{{linkedName shortname refid}}{{#unless @last}} / {{/unless}}{{/each}} + +{{/if}} +{{/with}} {{summary}} {{else}} # {{shortname name}} @@ -12,6 +18,17 @@ {{detaileddescription}} {{/if}} +{{#with (compoundsOfKind filtered.compounds "group") as |groups|}} +{{#if groups}} +### Groups + +| Name | Description | +|------|-------------| +{{#each groups}}| {{linkedName shortname refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + {{#with (compoundsOfKind filtered.compounds "namespace") as |namespaces|}} {{#if namespaces}} ### Namespaces diff --git a/templates/java/index.md b/templates/java/index.md index 622771ee..c750732d 100644 --- a/templates/java/index.md +++ b/templates/java/index.md @@ -1,11 +1,48 @@ # API Reference -{{#if filtered.compounds}} +{{#with (compoundsOfKind filtered.compounds "group") as |groups|}} +{{#if groups}} +### Groups + +| Name | Description | +|------|-------------| +{{#each groups}}| {{linkedName shortname refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "namespace") as |namespaces|}} +{{#if namespaces}} +### Namespaces + +| Name | Description | +|------|-------------| +{{#each namespaces}}| {{linkedName name refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "class" "struct" "interface") as |types|}} +{{#if types}} +### Classes + +| Name | Description | +|------|-------------| +{{#each types}}| {{linkedName name refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + +{{#with (compoundsOfKind filtered.compounds "enum") as |enums|}} +{{#if enums}} +### Enumerations + | Name | Description | |------|-------------| -{{#each filtered.compounds}}| [`{{shortname name}}`](#{{cleanId refid name}}) | {{cell summary}} | +{{#each enums}}| {{linkedName name refid}} | {{cell summary}} | {{/each}} {{/if}} +{{/with}} {{#each filtered.sections}} ## {{label}} diff --git a/templates/java/namespace.md b/templates/java/namespace.md index d9d7038f..4e21b365 100644 --- a/templates/java/namespace.md +++ b/templates/java/namespace.md @@ -1,15 +1,34 @@ {{cleanAnchor refid name}} -# {{shortname name}} - {{#if (eq kind "group")}} +# {{#if this.shortname}}{{this.shortname}}{{else}}{{shortname name}}{{/if}} + +{{#with (groupBreadcrumbs this) as |breadcrumbs|}} +{{#if breadcrumbs}} +> {{#each breadcrumbs}}{{linkedName shortname refid}}{{#unless @last}} / {{/unless}}{{/each}} + +{{/if}} +{{/with}} {{summary}} {{else}} +# {{shortname name}} + {{briefdescription}} {{detaileddescription}} {{/if}} +{{#with (compoundsOfKind filtered.compounds "group") as |groups|}} +{{#if groups}} +### Groups + +| Name | Description | +|------|-------------| +{{#each groups}}| {{linkedName shortname refid}} | {{cell summary}} | +{{/each}} +{{/if}} +{{/with}} + {{#with (compoundsOfKind filtered.compounds "namespace") as |namespaces|}} {{#if namespaces}} ### Namespaces diff --git a/test/fixtures/global-groups/Doxyfile b/test/fixtures/global-groups/Doxyfile new file mode 100644 index 00000000..dab71d0e --- /dev/null +++ b/test/fixtures/global-groups/Doxyfile @@ -0,0 +1,14 @@ +PROJECT_NAME = "Moxygen Global Groups Fixture" +OUTPUT_DIRECTORY = xml-out +INPUT = src +RECURSIVE = YES +FILE_PATTERNS = *.h +EXTRACT_ALL = YES +GENERATE_HTML = NO +GENERATE_LATEX = NO +GENERATE_XML = YES +XML_OUTPUT = xml +QUIET = YES +WARN_IF_UNDOCUMENTED = NO +FULL_PATH_NAMES = YES +STRIP_FROM_PATH = . diff --git a/test/fixtures/global-groups/src/global.h b/test/fixtures/global-groups/src/global.h new file mode 100644 index 00000000..bea75350 --- /dev/null +++ b/test/fixtures/global-groups/src/global.h @@ -0,0 +1,55 @@ +/** + This is a global define. +*/ +#define GDEFINE 1 + +/** + This is a global class. +*/ +class global_class { + public: + int global_class_a; +}; + +/** + This is a global variable. +*/ +global_class global_a; + +/** + @defgroup global_group Global Group + + This is the global group's description. + + @{ +*/ + +/** + This is a @ref global_group define. +*/ +#define GGROUP_DEFINE 1 + +/** + @defgroup nested_group Nested Group + @ingroup global_group + + This is the nested group's description. + + @{ +*/ + +/** + This is a nested grouped class. +*/ +class nested_class { + public: + int nested_class_a; +}; + +/** + @} +*/ + +/** + @} +*/ diff --git a/test/fixtures/global-groups/xml-out/xml/Doxyfile.xml b/test/fixtures/global-groups/xml-out/xml/Doxyfile.xml new file mode 100644 index 00000000..87e6bb6f --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/Doxyfile.xml @@ -0,0 +1,369 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/classglobal__class.xml b/test/fixtures/global-groups/xml-out/xml/classglobal__class.xml new file mode 100644 index 00000000..c835c101 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/classglobal__class.xml @@ -0,0 +1,32 @@ + + + + global_class + global.h + + + int + int global_class::global_class_a + + global_class_a + global_class::global_class_a + + + + + + + + + + + + +This is a global class. + + + + global_classglobal_class_a + + + diff --git a/test/fixtures/global-groups/xml-out/xml/classnested__class.xml b/test/fixtures/global-groups/xml-out/xml/classnested__class.xml new file mode 100644 index 00000000..58dbd5f2 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/classnested__class.xml @@ -0,0 +1,32 @@ + + + + nested_class + global.h + + + int + int nested_class::nested_class_a + + nested_class_a + nested_class::nested_class_a + + + + + + + + + + + + +This is a nested grouped class. + + + + nested_classnested_class_a + + + diff --git a/test/fixtures/global-groups/xml-out/xml/combine.xslt b/test/fixtures/global-groups/xml-out/xml/combine.xslt new file mode 100644 index 00000000..93973b6e --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/combine.xslt @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/compound.xsd b/test/fixtures/global-groups/xml-out/xml/compound.xsd new file mode 100644 index 00000000..7500b4b6 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/compound.xsd @@ -0,0 +1,1787 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The mentioned file will be located in the directory as specified by XML_OUTPUT + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml b/test/fixtures/global-groups/xml-out/xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml new file mode 100644 index 00000000..1e6dd515 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/dir_68267d1309a1af8e8297ef4c3efbcdba.xml @@ -0,0 +1,12 @@ + + + + src + global.h + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/doxyfile.xsd b/test/fixtures/global-groups/xml-out/xml/doxyfile.xsd new file mode 100644 index 00000000..579a8550 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/doxyfile.xsd @@ -0,0 +1,365 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/global_8h.xml b/test/fixtures/global-groups/xml-out/xml/global_8h.xml new file mode 100644 index 00000000..d0ff7213 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/global_8h.xml @@ -0,0 +1,66 @@ + + + + global.h + global_class + nested_class + + + GDEFINE + 1 + + + +This is a global define. + + + + + + GGROUP_DEFINE + + + + global_class + global_class global_a + + global_a + + + +This is a global variable. + + + + + + + + + + + + +#defineGDEFINE1 + +classglobal_class{ +public: +intglobal_class_a; +}; + +global_classglobal_a; + + +#defineGGROUP_DEFINE1 + + +classnested_class{ +public: +intnested_class_a; +}; + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/group__global__group.xml b/test/fixtures/global-groups/xml-out/xml/group__global__group.xml new file mode 100644 index 00000000..36ca12b5 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/group__global__group.xml @@ -0,0 +1,27 @@ + + + + global_group + Global Group + Nested Group + + + GGROUP_DEFINE + 1 + + + +This is a Global Group define. + + + + + + + + + +This is the global group's description. + + + diff --git a/test/fixtures/global-groups/xml-out/xml/group__nested__group.xml b/test/fixtures/global-groups/xml-out/xml/group__nested__group.xml new file mode 100644 index 00000000..38755467 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/group__nested__group.xml @@ -0,0 +1,13 @@ + + + + nested_group + Nested Group + nested_class + + + +This is the nested group's description. + + + diff --git a/test/fixtures/global-groups/xml-out/xml/index.xml b/test/fixtures/global-groups/xml-out/xml/index.xml new file mode 100644 index 00000000..b9090a18 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/index.xml @@ -0,0 +1,21 @@ + + + global_class + global_class_a + + nested_class + nested_class_a + + global.h + GDEFINE + GGROUP_DEFINE + global_a + + global_group + GGROUP_DEFINE + + nested_group + + src + + diff --git a/test/fixtures/global-groups/xml-out/xml/index.xsd b/test/fixtures/global-groups/xml-out/xml/index.xsd new file mode 100644 index 00000000..109c5795 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/index.xsd @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/fixtures/global-groups/xml-out/xml/xml.xsd b/test/fixtures/global-groups/xml-out/xml/xml.xsd new file mode 100644 index 00000000..9f80fe15 --- /dev/null +++ b/test/fixtures/global-groups/xml-out/xml/xml.xsd @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/test/integration.test.ts b/test/integration.test.ts index a2d4c241..9f72b20d 100644 --- a/test/integration.test.ts +++ b/test/integration.test.ts @@ -16,6 +16,7 @@ const programlistingLangXmlDir = join(import.meta.dirname, 'fixtures', 'programl const missingTagsXmlDir = join(import.meta.dirname, 'fixtures', 'missing-tags', 'xml-out', 'xml'); const memberKindsXmlDir = join(import.meta.dirname, 'fixtures', 'member-kinds', 'xml-out', 'xml'); const issue97XmlDir = join(import.meta.dirname, 'fixtures', 'issue-97', 'xml-out', 'xml'); +const globalGroupsXmlDir = join(import.meta.dirname, 'fixtures', 'global-groups', 'xml-out', 'xml'); const exampleOutputDir = join(outputRoot, 'example'); @@ -197,6 +198,126 @@ describe('integration', () => { expect(group).toContain('#### g1_a1'); }); + it('renders top-level groups, nested groups, and ungrouped globals without losing global classes', async () => { + const outputDir = join(outputRoot, 'global-groups'); + + await run({ + directory: globalGroupsXmlDir, + output: join(outputDir, '%s.md'), + groups: true, + anchors: true, + quiet: true, + }); + + const api = read(join(outputDir, 'api.md')); + expect(api).toContain('### Groups'); + expect(api).toContain('| [`Global Group`](global_group.md#globalgroup) | This is the global group'); + expect(api).toContain('### Classes'); + expect(api).toContain('| [`global_class`](#global_class) | This is a global class. |'); + expect(api).toContain('## global_class'); + expect(api).not.toContain('nested_class'); + + const group = read(join(outputDir, 'global_group.md')); + expect(group).toContain('# Global Group'); + expect(group).toContain('### Groups'); + expect(group).toContain('| [`Nested Group`](nested_group.md#nestedgroup) | This is the nested group'); + expect(group).not.toContain('global_class'); + expect(group).not.toContain('GDEFINE'); + + const nested = read(join(outputDir, 'nested_group.md')); + expect(nested).toContain('# Nested Group'); + expect(nested).toContain('> [`Global Group`](global_group.md#globalgroup)'); + expect(nested).toContain('## nested_class'); + + const pages = await generate({ + directory: globalGroupsXmlDir, + groups: true, + quiet: true, + }); + + const groupPage = pages.find((page) => page.kind === 'group' && page.slug === 'global_group'); + expect(groupPage).toBeDefined(); + expect(groupPage!.markdown).toContain('[`Nested Group`](nested_group.html#nestedgroup)'); + + const nestedGroupPage = pages.find((page) => page.kind === 'group' && page.slug === 'nested_group'); + expect(nestedGroupPage).toBeDefined(); + expect(nestedGroupPage!.markdown).toContain('> [`Global Group`](global_group.html#globalgroup)'); + }); + + it('writes standalone global class files when classes and groups are both enabled', async () => { + const outputDir = join(outputRoot, 'global-groups-classes'); + + await run({ + directory: globalGroupsXmlDir, + output: join(outputDir, '%s.md'), + groups: true, + classes: true, + anchors: true, + quiet: true, + }); + + expect(existsSync(join(outputDir, 'api.md'))).toBe(true); + expect(existsSync(join(outputDir, 'global_group.md'))).toBe(true); + expect(existsSync(join(outputDir, 'nested_group.md'))).toBe(true); + expect(existsSync(join(outputDir, 'global_class.md'))).toBe(true); + expect(existsSync(join(outputDir, 'nested_class.md'))).toBe(true); + + const api = read(join(outputDir, 'api.md')); + expect(api).toContain('| [`global_class`](global_class.md#global_class) | This is a global class. |'); + expect(api).not.toContain('## global_class'); + + const group = read(join(outputDir, 'global_group.md')); + expect(group).toContain('| [`Nested Group`](nested_group.md#nestedgroup) | This is the nested group'); + expect(group).not.toContain('## nested_class'); + + const nested = read(join(outputDir, 'nested_group.md')); + expect(nested).toContain('| [`nested_class`](nested_class.md#nested_class) | This is a nested grouped class. |'); + expect(nested).not.toContain('## nested_class'); + + const globalClass = read(join(outputDir, 'global_class.md')); + expect(globalClass).toContain('## global_class'); + expect(globalClass).toContain('This is a global class.'); + }); + + it('writes global class files when classes are enabled without namespaces', async () => { + const outputDir = join(outputRoot, 'global-classes'); + + await run({ + directory: globalGroupsXmlDir, + output: join(outputDir, '%s.md'), + classes: true, + anchors: true, + quiet: true, + }); + + expect(existsSync(join(outputDir, 'global_class.md'))).toBe(true); + expect(existsSync(join(outputDir, 'nested_class.md'))).toBe(true); + expect(existsSync(join(outputDir, 'api.md'))).toBe(true); + + const api = read(join(outputDir, 'api.md')); + expect(api).toContain('| [`global_class`](global_class.md#global_class) | This is a global class. |'); + expect(api).not.toContain('## global_class'); + + const globalClass = read(join(outputDir, 'global_class.md')); + expect(globalClass).toContain('## global_class'); + expect(globalClass).toContain('This is a global class.'); + }); + + it('keeps global-scope classes in default single-file output', async () => { + const output = join(outputRoot, 'global-single.md'); + + await run({ + directory: globalGroupsXmlDir, + output, + anchors: true, + quiet: true, + }); + + const content = read(output); + expect(content).toContain('## global_class'); + expect(content).toContain('This is a global class.'); + }); + it('keeps root-level classes from shared namespaces on grouped module pages', async () => { const outputDir = join(outputRoot, 'shared-grouped');