feat(macros): add reader for global Macros file
Importer for ProPresenter's protobuf-encoded `Macros` document. Exposes each macro's UUID and name plus the collections that group them. - src/MacroLibrary.php: top-level wrapper indexed by UUID and name - src/Macro.php, src/MacroCollection.php: per-entry wrappers - src/MacrosFileReader.php: file -> MacroLibrary entry point - bin/parse-macros.php: CLI listing macros and collections - tests/MacrosFileReaderTest.php: 10 tests against reference sample - doc/api/macros.md: API reference, plus INDEX/keywords updates
This commit is contained in:
parent
22ba4aff7d
commit
b30918af41
|
|
@ -19,6 +19,7 @@ All project documentation lives in `doc/`. Load only what you need.
|
|||
| Parse/modify `.pro` song files | `doc/api/song.md` |
|
||||
| Parse/modify `.proplaylist` files | `doc/api/playlist.md` |
|
||||
| Parse/modify `.probundle` files | `doc/api/bundle.md` |
|
||||
| Read the global `Macros` file | `doc/api/macros.md` |
|
||||
| Understand `.pro` binary format | `doc/formats/pp_song_spec.md` |
|
||||
| Understand `.proplaylist` binary format | `doc/formats/pp_playlist_spec.md` |
|
||||
| Understand `.probundle` binary format | `doc/formats/pp_bundle_spec.md` |
|
||||
|
|
@ -53,12 +54,14 @@ PHP tools for parsing, modifying, and generating ProPresenter 7 files:
|
|||
- **Songs** (`.pro`) — Protobuf-encoded presentation files with lyrics, groups, slides, arrangements, translations
|
||||
- **Playlists** (`.proplaylist`) — ZIP64 archives containing playlist metadata and embedded songs
|
||||
- **Bundles** (`.probundle`) — ZIP archives containing a single presentation with embedded media assets
|
||||
- **Macros** (`Macros`, no extension) — Global protobuf-encoded macro library with collections
|
||||
|
||||
### CLI Tools
|
||||
|
||||
```bash
|
||||
php bin/parse-song.php path/to/song.pro
|
||||
php bin/parse-playlist.php path/to/playlist.proplaylist
|
||||
php bin/parse-macros.php path/to/Macros
|
||||
```
|
||||
|
||||
### Key Source Files
|
||||
|
|
|
|||
66
bin/parse-macros.php
Executable file
66
bin/parse-macros.php
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use ProPresenter\Parser\MacrosFileReader;
|
||||
|
||||
if ($argc < 2) {
|
||||
echo "Usage: parse-macros.php <Macros>\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$filePath = $argv[1];
|
||||
|
||||
try {
|
||||
$library = MacrosFileReader::read($filePath);
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$macros = $library->getMacros();
|
||||
$collections = $library->getCollections();
|
||||
|
||||
echo "Macros (" . count($macros) . "):\n";
|
||||
foreach ($macros as $index => $macro) {
|
||||
$number = $index + 1;
|
||||
$name = $macro->getName();
|
||||
$uuid = $macro->getUuid();
|
||||
$actionCount = $macro->getActionCount();
|
||||
$startup = $macro->getTriggerOnStartup() ? ' (startup)' : '';
|
||||
|
||||
$memberships = $library->getCollectionsForMacro($macro);
|
||||
$collectionNames = array_map(fn ($c) => $c->getName(), $memberships);
|
||||
$collectionSuffix = $collectionNames === [] ? '' : ' [in: ' . implode(', ', $collectionNames) . ']';
|
||||
|
||||
$displayName = $name === '' ? '(unnamed)' : $name;
|
||||
echo " [" . $number . "] " . $displayName . " :: " . $uuid . " (" . $actionCount . " action" . ($actionCount !== 1 ? "s" : "") . ")" . $startup . $collectionSuffix . "\n";
|
||||
}
|
||||
|
||||
echo "\n";
|
||||
|
||||
if ($collections === []) {
|
||||
echo "Collections: (none)\n";
|
||||
exit(0);
|
||||
}
|
||||
|
||||
echo "Collections (" . count($collections) . "):\n";
|
||||
foreach ($collections as $index => $collection) {
|
||||
$number = $index + 1;
|
||||
$name = $collection->getName();
|
||||
$uuid = $collection->getUuid();
|
||||
$resolvedMacros = $library->getMacrosForCollection($collection);
|
||||
$count = count($resolvedMacros);
|
||||
|
||||
$displayName = $name === '' ? '(unnamed)' : $name;
|
||||
echo " [" . $number . "] " . $displayName . " :: " . $uuid . " (" . $count . " macro" . ($count !== 1 ? "s" : "") . ")\n";
|
||||
|
||||
foreach ($resolvedMacros as $macroIndex => $macro) {
|
||||
$macroNumber = $macroIndex + 1;
|
||||
$macroName = $macro->getName() === '' ? '(unnamed)' : $macro->getName();
|
||||
echo " " . $macroNumber . ". " . $macroName . " :: " . $macro->getUuid() . "\n";
|
||||
}
|
||||
}
|
||||
10
doc/INDEX.md
10
doc/INDEX.md
|
|
@ -9,6 +9,7 @@
|
|||
| Parse/modify `.pro` song files | [api/song.md](api/song.md) |
|
||||
| Parse/modify `.proplaylist` files | [api/playlist.md](api/playlist.md) |
|
||||
| Parse/modify `.probundle` files | [api/bundle.md](api/bundle.md) |
|
||||
| Read the global `Macros` file | [api/macros.md](api/macros.md) |
|
||||
| Understand `.pro` binary format | [formats/pp_song_spec.md](formats/pp_song_spec.md) |
|
||||
| Understand `.proplaylist` format | [formats/pp_playlist_spec.md](formats/pp_playlist_spec.md) |
|
||||
| Understand `.probundle` format | [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) |
|
||||
|
|
@ -28,6 +29,7 @@
|
|||
- [api/song.md](api/song.md) — Song parser API (read, modify, generate `.pro` files)
|
||||
- [api/playlist.md](api/playlist.md) — Playlist parser API (read, modify, generate `.proplaylist` files)
|
||||
- [api/bundle.md](api/bundle.md) — Bundle parser API (read, write `.probundle` files with media)
|
||||
- [api/macros.md](api/macros.md) — Macros library API (read the global `Macros` file)
|
||||
|
||||
### Internal Reference
|
||||
- [internal/learnings.md](internal/learnings.md) — Development learnings and conventions discovered
|
||||
|
|
@ -54,7 +56,8 @@ doc/
|
|||
├── api/ ← PHP API documentation
|
||||
│ ├── song.md
|
||||
│ ├── playlist.md
|
||||
│ └── bundle.md
|
||||
│ ├── bundle.md
|
||||
│ └── macros.md
|
||||
└── internal/ ← Development notes (optional context)
|
||||
├── learnings.md
|
||||
├── decisions.md
|
||||
|
|
@ -123,6 +126,8 @@ This project provides PHP tools to parse, modify, and generate ProPresenter 7 fi
|
|||
| `src/PresentationBundle.php` | Bundle wrapper (read/write `.probundle` files) |
|
||||
| `src/ProBundleReader.php` | Read `.probundle` files |
|
||||
| `src/ProBundleWriter.php` | Write `.probundle` files |
|
||||
| `src/MacroLibrary.php` | Macros library wrapper (read global `Macros` file) |
|
||||
| `src/MacrosFileReader.php` | Read global `Macros` file |
|
||||
|
||||
### CLI Tools
|
||||
|
||||
|
|
@ -132,4 +137,7 @@ php bin/parse-song.php path/to/song.pro
|
|||
|
||||
# Parse and display playlist structure
|
||||
php bin/parse-playlist.php path/to/playlist.proplaylist
|
||||
|
||||
# Parse and display the global Macros file
|
||||
php bin/parse-macros.php path/to/Macros
|
||||
```
|
||||
|
|
|
|||
151
doc/api/macros.md
Normal file
151
doc/api/macros.md
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
# Macros Library API
|
||||
|
||||
> PHP module for reading the global ProPresenter `Macros` file (raw protobuf,
|
||||
> no extension) and exposing each macro's name, UUID, and collection
|
||||
> membership.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\MacrosFileReader;
|
||||
|
||||
$library = MacrosFileReader::read('/path/to/Macros');
|
||||
|
||||
foreach ($library->getMacros() as $macro) {
|
||||
$macro->getName(); // "Gottesdienst START"
|
||||
$macro->getUuid(); // "FA0602E4-EDA2-4457-BB62-68AA17184217"
|
||||
|
||||
foreach ($library->getCollectionsForMacro($macro) as $collection) {
|
||||
$collection->getName(); // "Ablauf"
|
||||
$collection->getUuid(); // "8D02FC57-83F8-4042-9B90-81C229728426"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Layout
|
||||
|
||||
The `Macros` file is the protobuf-serialised
|
||||
[`MacrosDocument`](../../proto/macros.proto):
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `application_info` | message | ProPresenter version + flags that wrote the file |
|
||||
| `macros` | repeated `Macro` | Definitions: UUID, name, color, actions, icon, startup flag |
|
||||
| `macro_collections` | repeated `MacroCollection` | UUID, name, ordered list of `macro_id` references |
|
||||
|
||||
Macros and collections live at the document root. Membership is by UUID
|
||||
reference — a macro may appear in zero, one, or multiple collections.
|
||||
|
||||
---
|
||||
|
||||
## Reading
|
||||
|
||||
```php
|
||||
use ProPresenter\Parser\MacrosFileReader;
|
||||
|
||||
$library = MacrosFileReader::read('/Users/me/.../Macros');
|
||||
```
|
||||
|
||||
Throws `InvalidArgumentException` for missing files and `RuntimeException` for
|
||||
empty / unreadable files.
|
||||
|
||||
---
|
||||
|
||||
## MacroLibrary
|
||||
|
||||
Top-level wrapper around `Rv\Data\MacrosDocument`. Indexes macros and
|
||||
collections for fast lookup.
|
||||
|
||||
```php
|
||||
$library->getMacros(); // Macro[]
|
||||
$library->getMacroByUuid('FA06...'); // ?Macro (case-insensitive)
|
||||
$library->getMacroByName('Lied 1.Folie'); // ?Macro
|
||||
|
||||
$library->getCollections(); // MacroCollection[]
|
||||
$library->getCollectionByUuid('8D02...'); // ?MacroCollection (case-insensitive)
|
||||
$library->getCollectionByName('Ablauf'); // ?MacroCollection
|
||||
|
||||
// Cross-reference helpers
|
||||
$library->getMacrosForCollection($collection); // Macro[] in declared order
|
||||
$library->getCollectionsForMacro($macro); // MacroCollection[] (membership)
|
||||
|
||||
$library->getDocument(); // \Rv\Data\MacrosDocument (raw protobuf)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Macro
|
||||
|
||||
```php
|
||||
$macro->getUuid(); // "FA0602E4-..."
|
||||
$macro->getName(); // "Gottesdienst START"
|
||||
$macro->getColor(); // ['r'=>..,'g'=>..,'b'=>..,'a'=>..] | null
|
||||
$macro->getTriggerOnStartup(); // bool
|
||||
$macro->getActionCount(); // int — number of attached Action entries
|
||||
$macro->getImageType(); // int — see Rv\Data\MacrosDocument\Macro\ImageType
|
||||
$macro->getProto(); // \Rv\Data\MacrosDocument\Macro
|
||||
```
|
||||
|
||||
Action payloads are not unwrapped by this library; reach for `getProto()` and
|
||||
walk `getActions()` directly when needed.
|
||||
|
||||
---
|
||||
|
||||
## MacroCollection
|
||||
|
||||
```php
|
||||
$collection->getUuid(); // "8D02FC57-..."
|
||||
$collection->getName(); // "Ablauf"
|
||||
$collection->getMacroUuids(); // string[] — referenced macro UUIDs in order
|
||||
$collection->getProto(); // \Rv\Data\MacrosDocument\MacroCollection
|
||||
```
|
||||
|
||||
Items use a protobuf `oneof ItemType`; only `macro_id` is currently defined.
|
||||
Items without a populated reference are skipped.
|
||||
|
||||
---
|
||||
|
||||
## CLI Tool
|
||||
|
||||
```bash
|
||||
php bin/parse-macros.php /path/to/Macros
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
Macros (24):
|
||||
[1] Gottesdienst START :: FA0602E4-EDA2-4457-BB62-68AA17184217 (1 action) [in: Ablauf]
|
||||
...
|
||||
|
||||
Collections (3):
|
||||
[1] Ablauf :: 8D02FC57-83F8-4042-9B90-81C229728426 (12 macros)
|
||||
1. Gottesdienst START :: FA0602E4-EDA2-4457-BB62-68AA17184217
|
||||
...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/MacroLibrary.php` | Document-level wrapper with lookup helpers |
|
||||
| `src/Macro.php` | Single macro wrapper |
|
||||
| `src/MacroCollection.php` | Collection wrapper |
|
||||
| `src/MacrosFileReader.php` | Reads the `Macros` file |
|
||||
| `bin/parse-macros.php` | CLI tool |
|
||||
| `proto/macros.proto` | Protobuf schema |
|
||||
| `generated/Rv/Data/MacrosDocument.php` | Generated message classes |
|
||||
|
||||
---
|
||||
|
||||
## Scope Notes
|
||||
|
||||
This module is read-only by design. Action editing, slide-side macro
|
||||
references on `.pro` files (see `Slide::getMacroUuid()` /
|
||||
`Slide::setMacro()`), and writing the `Macros` file back are not implemented
|
||||
here. Add them by mirroring the `ProFileWriter` / `ProFileGenerator` pattern
|
||||
when needed.
|
||||
|
|
@ -66,7 +66,10 @@
|
|||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| macro | [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| macro | [api/macros.md](api/macros.md), [api/song.md](api/song.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5 |
|
||||
| Macros file | [api/macros.md](api/macros.md) |
|
||||
| MacroCollection | [api/macros.md](api/macros.md) |
|
||||
| MacroLibrary | [api/macros.md](api/macros.md) |
|
||||
| media | [api/song.md](api/song.md), [api/bundle.md](api/bundle.md), [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
|
||||
| image | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 3 |
|
||||
| video | [formats/pp_song_spec.md](formats/pp_song_spec.md) Section 5, [formats/pp_bundle_spec.md](formats/pp_bundle_spec.md) Section 6 |
|
||||
|
|
@ -77,10 +80,11 @@
|
|||
|
||||
| Keyword | Document |
|
||||
|---------|----------|
|
||||
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
|
||||
| read | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md) |
|
||||
| write | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
|
||||
| generate | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md) |
|
||||
| parse | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/bundle.md](api/bundle.md), [api/macros.md](api/macros.md) |
|
||||
| MacrosFileReader | [api/macros.md](api/macros.md) |
|
||||
| ProFileReader | [api/song.md](api/song.md) |
|
||||
| ProFileWriter | [api/song.md](api/song.md) |
|
||||
| ProFileGenerator | [api/song.md](api/song.md) |
|
||||
|
|
@ -92,8 +96,8 @@
|
|||
| PresentationBundle | [api/bundle.md](api/bundle.md) |
|
||||
| Song | [api/song.md](api/song.md) |
|
||||
| PlaylistArchive | [api/playlist.md](api/playlist.md) |
|
||||
| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md) |
|
||||
| CLI | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md) |
|
||||
| command line | [api/song.md](api/song.md), [api/playlist.md](api/playlist.md), [api/macros.md](api/macros.md) |
|
||||
|
||||
## Protobuf
|
||||
|
||||
|
|
|
|||
BIN
doc/reference_samples/Macros
Normal file
BIN
doc/reference_samples/Macros
Normal file
Binary file not shown.
95
src/Macro.php
Normal file
95
src/Macro.php
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\MacrosDocument\Macro as MacroProto;
|
||||
|
||||
/**
|
||||
* Wraps a protobuf Macro, exposing its identifying fields (UUID, name) plus
|
||||
* convenience metadata (color, action count, image type, startup flag).
|
||||
*
|
||||
* Macros live in the global ProPresenter `Macros` document and may belong to
|
||||
* one or more {@see MacroCollection}s. Membership is resolved by
|
||||
* {@see MacroLibrary}, not by this wrapper.
|
||||
*/
|
||||
class Macro
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MacroProto $macro,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the macro's UUID as an upper-case string (empty when unset).
|
||||
*/
|
||||
public function getUuid(): string
|
||||
{
|
||||
return $this->macro->getUuid()?->getString() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the macro's display name.
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->macro->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the macro's color as an associative array, or null when unset.
|
||||
*
|
||||
* @return array{r: float, g: float, b: float, a: float}|null
|
||||
*/
|
||||
public function getColor(): ?array
|
||||
{
|
||||
if (!$this->macro->hasColor()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$color = $this->macro->getColor();
|
||||
|
||||
return [
|
||||
'r' => $color->getRed(),
|
||||
'g' => $color->getGreen(),
|
||||
'b' => $color->getBlue(),
|
||||
'a' => $color->getAlpha(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the macro is configured to fire on application startup.
|
||||
*/
|
||||
public function getTriggerOnStartup(): bool
|
||||
{
|
||||
return $this->macro->getTriggerOnStartup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of action entries attached to this macro.
|
||||
*
|
||||
* Action payloads are not exposed by this wrapper; use {@see getProto()} to
|
||||
* inspect them via the generated protobuf classes.
|
||||
*/
|
||||
public function getActionCount(): int
|
||||
{
|
||||
return count($this->macro->getActions());
|
||||
}
|
||||
|
||||
/**
|
||||
* Icon enum value (see {@see \Rv\Data\MacrosDocument\Macro\ImageType}).
|
||||
*/
|
||||
public function getImageType(): int
|
||||
{
|
||||
return $this->macro->getImageType();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying protobuf Macro message.
|
||||
*/
|
||||
public function getProto(): MacroProto
|
||||
{
|
||||
return $this->macro;
|
||||
}
|
||||
}
|
||||
64
src/MacroCollection.php
Normal file
64
src/MacroCollection.php
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\MacrosDocument\MacroCollection as MacroCollectionProto;
|
||||
|
||||
/**
|
||||
* Wraps a protobuf MacroCollection. A collection groups macros by UUID
|
||||
* reference; macro definitions themselves live at the document root.
|
||||
*/
|
||||
class MacroCollection
|
||||
{
|
||||
public function __construct(
|
||||
private readonly MacroCollectionProto $collection,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection's UUID as a string (empty when unset).
|
||||
*/
|
||||
public function getUuid(): string
|
||||
{
|
||||
return $this->collection->getUuid()?->getString() ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collection's display name (e.g. "Ablauf").
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->collection->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the UUIDs of macros referenced by this collection, in order.
|
||||
*
|
||||
* Items in the protobuf use a `oneof` ItemType — currently only
|
||||
* `macro_id` is defined. Items without a populated reference are skipped.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getMacroUuids(): array
|
||||
{
|
||||
$uuids = [];
|
||||
foreach ($this->collection->getItems() as $item) {
|
||||
$macroId = $item->getMacroId();
|
||||
if ($macroId !== null) {
|
||||
$uuids[] = $macroId->getString();
|
||||
}
|
||||
}
|
||||
|
||||
return $uuids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying protobuf MacroCollection message.
|
||||
*/
|
||||
public function getProto(): MacroCollectionProto
|
||||
{
|
||||
return $this->collection;
|
||||
}
|
||||
}
|
||||
163
src/MacroLibrary.php
Normal file
163
src/MacroLibrary.php
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use Rv\Data\MacrosDocument;
|
||||
|
||||
/**
|
||||
* Wraps a protobuf MacrosDocument — the global ProPresenter `Macros` file
|
||||
* which lists every macro definition and the collections that group them.
|
||||
*
|
||||
* Lookup helpers index macros and collections by UUID (case-insensitive) and
|
||||
* by name, mirroring the convention used by {@see Song}. Collection
|
||||
* membership is resolved by mapping {@see MacroCollection::getMacroUuids()}
|
||||
* back through this library.
|
||||
*/
|
||||
class MacroLibrary
|
||||
{
|
||||
/** @var Macro[] */
|
||||
private array $macros = [];
|
||||
|
||||
/** @var MacroCollection[] */
|
||||
private array $collections = [];
|
||||
|
||||
/** @var array<string, Macro> */
|
||||
private array $macrosByUuid = [];
|
||||
|
||||
/** @var array<string, Macro> */
|
||||
private array $macrosByName = [];
|
||||
|
||||
/** @var array<string, MacroCollection> */
|
||||
private array $collectionsByUuid = [];
|
||||
|
||||
/** @var array<string, MacroCollection> */
|
||||
private array $collectionsByName = [];
|
||||
|
||||
/** @var array<string, MacroCollection[]> */
|
||||
private array $collectionsByMacroUuid = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly MacrosDocument $document,
|
||||
) {
|
||||
foreach ($this->document->getMacros() as $macroProto) {
|
||||
$macro = new Macro($macroProto);
|
||||
$this->macros[] = $macro;
|
||||
|
||||
$uuid = strtoupper($macro->getUuid());
|
||||
if ($uuid !== '') {
|
||||
$this->macrosByUuid[$uuid] = $macro;
|
||||
}
|
||||
|
||||
$name = $macro->getName();
|
||||
if ($name !== '') {
|
||||
$this->macrosByName[$name] = $macro;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->document->getMacroCollections() as $collectionProto) {
|
||||
$collection = new MacroCollection($collectionProto);
|
||||
$this->collections[] = $collection;
|
||||
|
||||
$uuid = strtoupper($collection->getUuid());
|
||||
if ($uuid !== '') {
|
||||
$this->collectionsByUuid[$uuid] = $collection;
|
||||
}
|
||||
|
||||
$name = $collection->getName();
|
||||
if ($name !== '') {
|
||||
$this->collectionsByName[$name] = $collection;
|
||||
}
|
||||
|
||||
foreach ($collection->getMacroUuids() as $macroUuid) {
|
||||
$key = strtoupper($macroUuid);
|
||||
if ($key === '') {
|
||||
continue;
|
||||
}
|
||||
$this->collectionsByMacroUuid[$key][] = $collection;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Macro[]
|
||||
*/
|
||||
public function getMacros(): array
|
||||
{
|
||||
return $this->macros;
|
||||
}
|
||||
|
||||
public function getMacroByUuid(string $uuid): ?Macro
|
||||
{
|
||||
return $this->macrosByUuid[strtoupper($uuid)] ?? null;
|
||||
}
|
||||
|
||||
public function getMacroByName(string $name): ?Macro
|
||||
{
|
||||
return $this->macrosByName[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MacroCollection[]
|
||||
*/
|
||||
public function getCollections(): array
|
||||
{
|
||||
return $this->collections;
|
||||
}
|
||||
|
||||
public function getCollectionByUuid(string $uuid): ?MacroCollection
|
||||
{
|
||||
return $this->collectionsByUuid[strtoupper($uuid)] ?? null;
|
||||
}
|
||||
|
||||
public function getCollectionByName(string $name): ?MacroCollection
|
||||
{
|
||||
return $this->collectionsByName[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a collection's referenced macros to {@see Macro} wrappers.
|
||||
*
|
||||
* Unknown UUIDs (referenced but not defined at document root) are skipped.
|
||||
*
|
||||
* @return Macro[]
|
||||
*/
|
||||
public function getMacrosForCollection(MacroCollection $collection): array
|
||||
{
|
||||
$resolved = [];
|
||||
foreach ($collection->getMacroUuids() as $uuid) {
|
||||
$macro = $this->getMacroByUuid($uuid);
|
||||
if ($macro !== null) {
|
||||
$resolved[] = $macro;
|
||||
}
|
||||
}
|
||||
|
||||
return $resolved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the collections a macro belongs to (membership is by UUID
|
||||
* reference). A macro may legally appear in zero, one, or many
|
||||
* collections.
|
||||
*
|
||||
* @return MacroCollection[]
|
||||
*/
|
||||
public function getCollectionsForMacro(Macro $macro): array
|
||||
{
|
||||
$key = strtoupper($macro->getUuid());
|
||||
if ($key === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->collectionsByMacroUuid[$key] ?? [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the underlying protobuf MacrosDocument.
|
||||
*/
|
||||
public function getDocument(): MacrosDocument
|
||||
{
|
||||
return $this->document;
|
||||
}
|
||||
}
|
||||
42
src/MacrosFileReader.php
Normal file
42
src/MacrosFileReader.php
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use RuntimeException;
|
||||
use Rv\Data\MacrosDocument;
|
||||
|
||||
/**
|
||||
* Reader for the global ProPresenter `Macros` file (a raw protobuf
|
||||
* serialisation of {@see MacrosDocument}, no extension).
|
||||
*/
|
||||
final class MacrosFileReader
|
||||
{
|
||||
public static function read(string $filePath): MacroLibrary
|
||||
{
|
||||
if ($filePath === '' || !is_file($filePath)) {
|
||||
throw new InvalidArgumentException(sprintf('Macros file not found: %s', $filePath));
|
||||
}
|
||||
|
||||
$size = filesize($filePath);
|
||||
if ($size === false) {
|
||||
throw new RuntimeException(sprintf('Unable to determine file size: %s', $filePath));
|
||||
}
|
||||
|
||||
if ($size === 0) {
|
||||
throw new RuntimeException(sprintf('Macros file is empty: %s', $filePath));
|
||||
}
|
||||
|
||||
$data = file_get_contents($filePath);
|
||||
if ($data === false) {
|
||||
throw new RuntimeException(sprintf('Unable to read macros file: %s', $filePath));
|
||||
}
|
||||
|
||||
$document = new MacrosDocument();
|
||||
$document->mergeFromString($data);
|
||||
|
||||
return new MacroLibrary($document);
|
||||
}
|
||||
}
|
||||
138
tests/MacrosFileReaderTest.php
Normal file
138
tests/MacrosFileReaderTest.php
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace ProPresenter\Parser\Tests;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ProPresenter\Parser\Macro;
|
||||
use ProPresenter\Parser\MacroCollection;
|
||||
use ProPresenter\Parser\MacroLibrary;
|
||||
use ProPresenter\Parser\MacrosFileReader;
|
||||
|
||||
class MacrosFileReaderTest extends TestCase
|
||||
{
|
||||
private const REFERENCE_PATH = __DIR__ . '/../doc/reference_samples/Macros';
|
||||
|
||||
#[Test]
|
||||
public function readThrowsOnMissingFile(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
MacrosFileReader::read(__DIR__ . '/../doc/reference_samples/does-not-exist-macros');
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function readReturnsMacroLibraryWithExpectedCounts(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$this->assertInstanceOf(MacroLibrary::class, $library);
|
||||
$this->assertCount(24, $library->getMacros());
|
||||
$this->assertCount(3, $library->getCollections());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function macrosExposeNameAndUuid(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$first = $library->getMacros()[0];
|
||||
$this->assertInstanceOf(Macro::class, $first);
|
||||
$this->assertSame('Gottesdienst START', $first->getName());
|
||||
$this->assertSame('FA0602E4-EDA2-4457-BB62-68AA17184217', $first->getUuid());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function macroLookupByUuidIsCaseInsensitive(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$upper = $library->getMacroByUuid('FA0602E4-EDA2-4457-BB62-68AA17184217');
|
||||
$lower = $library->getMacroByUuid('fa0602e4-eda2-4457-bb62-68aa17184217');
|
||||
|
||||
$this->assertNotNull($upper);
|
||||
$this->assertSame($upper, $lower);
|
||||
$this->assertSame('Gottesdienst START', $upper->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function macroLookupByNameSucceeds(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$macro = $library->getMacroByName('Predigt - Text Lang');
|
||||
$this->assertNotNull($macro);
|
||||
$this->assertSame('0A1543A9-7881-4537-982C-2933AA5472F8', $macro->getUuid());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collectionsExposeNameUuidAndOrderedMacroUuids(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$ablauf = $library->getCollectionByName('Ablauf');
|
||||
$this->assertInstanceOf(MacroCollection::class, $ablauf);
|
||||
$this->assertSame('8D02FC57-83F8-4042-9B90-81C229728426', $ablauf->getUuid());
|
||||
|
||||
$uuids = $ablauf->getMacroUuids();
|
||||
$this->assertCount(12, $uuids);
|
||||
$this->assertSame('FA0602E4-EDA2-4457-BB62-68AA17184217', $uuids[0]);
|
||||
$this->assertSame('5ADFBB7A-1529-42B9-A9C6-77B7D01C4715', $uuids[11]);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function collectionLookupByUuidIsCaseInsensitive(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$upper = $library->getCollectionByUuid('AD18A4F6-135F-4A52-B92B-CA6619A55A9B');
|
||||
$lower = $library->getCollectionByUuid('ad18a4f6-135f-4a52-b92b-ca6619a55a9b');
|
||||
|
||||
$this->assertNotNull($upper);
|
||||
$this->assertSame($upper, $lower);
|
||||
$this->assertSame('AbsoluteTimer', $upper->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getMacrosForCollectionResolvesReferences(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$absoluteTimer = $library->getCollectionByName('AbsoluteTimer');
|
||||
$this->assertNotNull($absoluteTimer);
|
||||
|
||||
$macros = $library->getMacrosForCollection($absoluteTimer);
|
||||
$this->assertCount(2, $macros);
|
||||
$this->assertSame('Doors Open - 9:45', $macros[0]->getName());
|
||||
$this->assertSame('Godi START - 10:02', $macros[1]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function getCollectionsForMacroReturnsMembership(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$macro = $library->getMacroByUuid('FA0602E4-EDA2-4457-BB62-68AA17184217');
|
||||
$this->assertNotNull($macro);
|
||||
|
||||
$collections = $library->getCollectionsForMacro($macro);
|
||||
$this->assertCount(1, $collections);
|
||||
$this->assertSame('Ablauf', $collections[0]->getName());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function startupMacroFlagSurfaces(): void
|
||||
{
|
||||
$library = MacrosFileReader::read(self::REFERENCE_PATH);
|
||||
|
||||
$startup = array_values(array_filter(
|
||||
$library->getMacros(),
|
||||
fn (Macro $m) => $m->getTriggerOnStartup(),
|
||||
));
|
||||
|
||||
$this->assertCount(1, $startup);
|
||||
$this->assertSame('Doors Open - 9:45', $startup[0]->getName());
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue