Skip to content

Commit f3d1181

Browse files
committed
feat(api): added theme colors
1 parent aa6d8c3 commit f3d1181

File tree

6 files changed

+106
-2
lines changed

6 files changed

+106
-2
lines changed

docs/openapi.json

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -945,7 +945,7 @@
945945
}
946946
}
947947
},
948-
"/api/v4.0/faqs/latest": {
948+
"v4.0/faqs/latest": {
949949
"get": {
950950
"tags": ["Public Endpoints"],
951951
"description": "This endpoint returns the latest FAQs for the given language provided by \"Accept-Language\".",
@@ -1901,6 +1901,23 @@
19011901
"type": "string",
19021902
"example": "https://localhost/assets/images/logo-transparent.svg"
19031903
},
1904+
"themeColors": {
1905+
"type": "object",
1906+
"example": {
1907+
"light": {
1908+
"--bs-primary": "#083c83",
1909+
"--bs-body-bg": "#ffffff"
1910+
},
1911+
"dark": {
1912+
"--bs-primary": "#083c83",
1913+
"--bs-body-bg": "var(--bs-dark)"
1914+
},
1915+
"highContrast": {
1916+
"--bs-primary": "#ffff00",
1917+
"--bs-body-bg": "#000000"
1918+
}
1919+
}
1920+
},
19041921
"oauthDiscovery": {
19051922
"type": "object"
19061923
}

docs/openapi.yaml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,7 @@ paths:
681681
application/json:
682682
schema: {}
683683
example: []
684-
/api/v4.0/faqs/latest:
684+
v4.0/faqs/latest:
685685
get:
686686
tags:
687687
- 'Public Endpoints'
@@ -1350,6 +1350,16 @@ paths:
13501350
availableLanguages: { type: object, example: { de: German, en: English } }
13511351
enabledFeatures: { type: object }
13521352
publicLogoUrl: { type: string, example: 'https://localhost/assets/images/logo-transparent.svg' }
1353+
themeColors:
1354+
{
1355+
type: object,
1356+
example:
1357+
{
1358+
light: { '--bs-primary': '#083c83', '--bs-body-bg': '#ffffff' },
1359+
dark: { '--bs-primary': '#083c83', '--bs-body-bg': var(--bs-dark) },
1360+
highContrast: { '--bs-primary': '#ffff00', '--bs-body-bg': '#000000' },
1361+
},
1362+
}
13531363
oauthDiscovery: { type: object }
13541364
type: object
13551365
/api/v4.0/news:

phpmyfaq/src/phpMyFAQ/Api/MetaService.php

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ public function __construct(
3838
* availableLanguages: array<string, string>,
3939
* enabledFeatures: array<string, bool>,
4040
* publicLogoUrl: string,
41+
* themeColors: array<string, array<string, string>>,
4142
* oauthDiscovery: array<string, bool|string|string[]>
4243
* }
4344
*/
@@ -50,6 +51,7 @@ public function getPublicMetadata(): array
5051
'availableLanguages' => LanguageHelper::getAvailableLanguages(),
5152
'enabledFeatures' => $this->buildEnabledFeatures(),
5253
'publicLogoUrl' => $this->buildPublicLogoUrl(),
54+
'themeColors' => $this->buildThemeColors(),
5355
'oauthDiscovery' => $this->oAuthDiscoveryService->getMetaDiscovery(),
5456
];
5557
}
@@ -76,6 +78,61 @@ private function buildPublicLogoUrl(): string
7678
return rtrim($this->configuration->getDefaultUrl(), characters: '/') . '/assets/images/logo-transparent.svg';
7779
}
7880

81+
/**
82+
* @return array<string, array<string, string>>
83+
*/
84+
private function buildThemeColors(): array
85+
{
86+
$themeCssPath = PMF_ROOT_DIR . '/assets/templates/default/theme.css';
87+
if (!is_readable($themeCssPath)) {
88+
return [
89+
'light' => [],
90+
'dark' => [],
91+
'highContrast' => [],
92+
];
93+
}
94+
95+
$themeCss = file_get_contents($themeCssPath);
96+
if ($themeCss === false) {
97+
return [
98+
'light' => [],
99+
'dark' => [],
100+
'highContrast' => [],
101+
];
102+
}
103+
104+
return [
105+
'light' => $this->extractThemeVariables($themeCss, ":root,\n[data-bs-theme='light']"),
106+
'dark' => $this->extractThemeVariables($themeCss, "[data-bs-theme='dark']"),
107+
'highContrast' => $this->extractThemeVariables($themeCss, "[data-bs-theme='high-contrast']"),
108+
];
109+
}
110+
111+
/**
112+
* @return array<string, string>
113+
*/
114+
private function extractThemeVariables(string $themeCss, string $selector): array
115+
{
116+
$pattern = sprintf('/%s\s*\{(?P<body>.*?)^\}/ms', preg_quote($selector, delimiter: '/'));
117+
if (preg_match($pattern, $themeCss, $matches) !== 1) {
118+
return [];
119+
}
120+
121+
preg_match_all(
122+
'/(?P<name>--[A-Za-z0-9\-]+)\s*:\s*(?P<value>[^;]+);/',
123+
$matches['body'],
124+
$variableMatches,
125+
PREG_SET_ORDER,
126+
);
127+
128+
$variables = [];
129+
foreach ($variableMatches as $variableMatch) {
130+
$variables[$variableMatch['name']] = trim($variableMatch['value']);
131+
}
132+
133+
return $variables;
134+
}
135+
79136
private function toBool(mixed $value): bool
80137
{
81138
return $value === true || $value === 1 || $value === '1' || $value === 'true';

phpmyfaq/src/phpMyFAQ/Controller/Api/MetaController.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,20 @@ public function __construct(?MetaService $metaService = null)
6262
type: 'string',
6363
example: 'https://localhost/assets/images/logo-transparent.svg',
6464
),
65+
new OA\Property(property: 'themeColors', type: 'object', example: [
66+
'light' => [
67+
'--bs-primary' => '#083c83',
68+
'--bs-body-bg' => '#ffffff',
69+
],
70+
'dark' => [
71+
'--bs-primary' => '#083c83',
72+
'--bs-body-bg' => 'var(--bs-dark)',
73+
],
74+
'highContrast' => [
75+
'--bs-primary' => '#ffff00',
76+
'--bs-body-bg' => '#000000',
77+
],
78+
]),
6579
new OA\Property(property: 'oauthDiscovery', type: 'object'),
6680
]),
6781
)]

tests/phpMyFAQ/Api/MetaServiceTest.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ public function testGetPublicMetadataReturnsExpectedPayload(): void
7676
$this->assertFalse($payload['enabledFeatures']['opensearch']);
7777
$this->assertFalse($payload['enabledFeatures']['signInWithMicrosoft']);
7878
$this->assertStringEndsWith('/assets/images/logo-transparent.svg', $payload['publicLogoUrl']);
79+
$this->assertSame('#083c83', $payload['themeColors']['light']['--bs-primary']);
80+
$this->assertSame('#ffffff', $payload['themeColors']['light']['--bs-body-bg']);
81+
$this->assertSame('var(--bs-dark)', $payload['themeColors']['dark']['--bs-body-bg']);
82+
$this->assertSame('#ffff00', $payload['themeColors']['highContrast']['--bs-primary']);
7983
$this->assertTrue($payload['oauthDiscovery']['enabled']);
8084
$this->assertStringEndsWith('/api', $payload['oauthDiscovery']['issuer']);
8185
$this->assertStringEndsWith('/api/oauth/authorize', $payload['oauthDiscovery']['authorizationEndpoint']);

tests/phpMyFAQ/Controller/Api/MetaControllerWebTest.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ public function testMetaEndpointReturnsJsonMetadata(): void
7878
self::assertSame('application/json', $response->headers->get('Content-Type'));
7979
self::assertIsArray($payload);
8080
self::assertStringEndsWith('/assets/images/logo-transparent.svg', $payload['publicLogoUrl']);
81+
self::assertSame('#083c83', $payload['themeColors']['light']['--bs-primary']);
82+
self::assertSame('#000000', $payload['themeColors']['highContrast']['--bs-body-bg']);
8183
self::assertTrue($payload['enabledFeatures']['api']);
8284
self::assertTrue($payload['oauthDiscovery']['enabled']);
8385
self::assertStringEndsWith('/api/oauth/token', $payload['oauthDiscovery']['tokenEndpoint']);

0 commit comments

Comments
 (0)