implements PLN-0068

This commit is contained in:
bQUARKz 2026-05-06 13:46:23 +01:00
parent baf9f90c93
commit 54f8c0d6d8
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
6 changed files with 199 additions and 14 deletions

View File

@ -1,7 +1,10 @@
package p.studio.compiler;
import p.studio.compiler.models.FrontendSpec;
import p.studio.compiler.models.FrontendEditorPaletteSpec;
import p.studio.compiler.models.FrontendSemanticPresentationSpec;
import p.studio.compiler.models.FrontendTokenStyleSpec;
import p.studio.compiler.models.FrontendVisualThemeSpec;
import p.studio.compiler.pbs.PbsSemanticKind;
import p.studio.utilities.structures.ReadOnlySet;
@ -16,6 +19,41 @@ public class PBSDefinitions {
.stdlibVersions(List.of(FrontendSpec.Stdlib.asDefault(1)))
.semanticPresentation(new FrontendSemanticPresentationSpec(
PbsSemanticKind.semanticKeys(),
List.of("/themes/pbs/semantic-highlighting.css")))
List.of(new FrontendVisualThemeSpec(
"pbs-default",
"PBS Default",
new FrontendEditorPaletteSpec(
"#d9e2ec",
"#2d4f6b",
"#2d4f6b",
"#6f8193",
"#2c2140",
"#a47dff",
"#efe6ff"),
List.of(
new FrontendTokenStyleSpec("pbs-comment", "#c090b0", true, false, false),
new FrontendTokenStyleSpec("pbs-string", "#00c088", false, false, false),
new FrontendTokenStyleSpec("pbs-number", "#ff90b0", false, false, false),
new FrontendTokenStyleSpec("pbs-literal", "#4fc1ff", false, false, false),
new FrontendTokenStyleSpec("pbs-lifecycle", "#ef50c0", false, false, false),
new FrontendTokenStyleSpec("pbs-keyword", "#569cd6", false, false, false),
new FrontendTokenStyleSpec("pbs-operator", "#d4d4d4", false, false, false),
new FrontendTokenStyleSpec("pbs-punctuation", "#d4d4d4", false, false, false),
new FrontendTokenStyleSpec("pbs-function", "#f2c14e", false, false, false),
new FrontendTokenStyleSpec("pbs-method", "#f2c14e", false, false, false),
new FrontendTokenStyleSpec("pbs-constructor", "#ecdcaa", false, false, false),
new FrontendTokenStyleSpec("pbs-struct", "#4ec9b0", false, false, false),
new FrontendTokenStyleSpec("pbs-contract", "#78dce8", false, false, false),
new FrontendTokenStyleSpec("pbs-host", "#b7a2fa", false, false, false),
new FrontendTokenStyleSpec("pbs-builtin-type", "#8be9fd", false, false, false),
new FrontendTokenStyleSpec("pbs-service", "#b7a2fa", false, false, false),
new FrontendTokenStyleSpec("pbs-error", "#ff5b5b", false, false, false),
new FrontendTokenStyleSpec("pbs-enum", "#56cfe1", false, false, false),
new FrontendTokenStyleSpec("pbs-callback", "#b7a2fa", false, false, false),
new FrontendTokenStyleSpec("pbs-global", "#f790fc", true, false, false),
new FrontendTokenStyleSpec("pbs-const", "#f78c6c", false, false, false),
new FrontendTokenStyleSpec("pbs-implements", "#a1c181", false, false, false),
new FrontendTokenStyleSpec("pbs-identifier", "#d4d4d4", false, false, false)))),
"pbs-default"))
.build();
}

View File

@ -3,11 +3,8 @@ package p.studio.compiler.pbs;
import org.junit.jupiter.api.Test;
import p.studio.compiler.PBSDefinitions;
import java.net.URL;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class PbsSemanticPresentationContractTest {
@ -18,16 +15,21 @@ final class PbsSemanticPresentationContractTest {
assertFalse(presentation.semanticKeys().isEmpty());
assertEquals(PbsSemanticKind.semanticKeys(), presentation.semanticKeys());
assertEquals(1, presentation.resources().size());
assertEquals("/themes/pbs/semantic-highlighting.css", presentation.resources().getFirst());
assertEquals("pbs-default", presentation.defaultThemeId());
assertEquals(1, presentation.themes().size());
assertEquals("PBS Default", presentation.defaultTheme().displayName());
assertEquals("#d9e2ec", presentation.defaultTheme().editorPalette().baseForeground());
assertEquals("#569cd6", presentation.defaultTheme().requireTokenStyle("pbs-keyword").foreground());
assertTrue(presentation.defaultTheme().requireTokenStyle("pbs-comment").italic());
}
@Test
void shouldPublishResolvableSemanticPresentationResources() {
final var resourcePath = PBSDefinitions.PBS.getSemanticPresentation().resources().getFirst();
final URL resource = PBSDefinitions.class.getResource(resourcePath);
void shouldCoverTheWholePbsSemanticVocabularyInTheDefaultTheme() {
final var presentation = PBSDefinitions.PBS.getSemanticPresentation();
final var defaultTheme = presentation.defaultTheme();
assertNotNull(resource, resourcePath);
assertTrue(resource.toExternalForm().endsWith("themes/pbs/semantic-highlighting.css"));
assertEquals(
PbsSemanticKind.semanticKeys(),
defaultTheme.tokenStyles().stream().map(tokenStyle -> tokenStyle.semanticKey()).toList());
}
}

View File

@ -0,0 +1,33 @@
package p.studio.compiler.models;
import java.util.Objects;
public record FrontendEditorPaletteSpec(
String baseForeground,
String selectionBackground,
String activeHighlightBackground,
String lineNumberForeground,
String statusChipBackground,
String statusChipBorder,
String statusChipForeground) {
public FrontendEditorPaletteSpec {
baseForeground = requireColor(baseForeground, "baseForeground");
selectionBackground = requireColor(selectionBackground, "selectionBackground");
activeHighlightBackground = requireColor(activeHighlightBackground, "activeHighlightBackground");
lineNumberForeground = requireColor(lineNumberForeground, "lineNumberForeground");
statusChipBackground = requireColor(statusChipBackground, "statusChipBackground");
statusChipBorder = requireColor(statusChipBorder, "statusChipBorder");
statusChipForeground = requireColor(statusChipForeground, "statusChipForeground");
}
private static String requireColor(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -5,14 +5,53 @@ import java.util.Objects;
public record FrontendSemanticPresentationSpec(
List<String> semanticKeys,
List<String> resources) {
List<FrontendVisualThemeSpec> themes,
String defaultThemeId) {
public FrontendSemanticPresentationSpec {
semanticKeys = List.copyOf(Objects.requireNonNull(semanticKeys, "semanticKeys"));
resources = List.copyOf(Objects.requireNonNull(resources, "resources"));
themes = List.copyOf(Objects.requireNonNull(themes, "themes"));
defaultThemeId = Objects.requireNonNull(defaultThemeId, "defaultThemeId").trim();
if (semanticKeys.isEmpty() && themes.isEmpty()) {
if (!defaultThemeId.isEmpty()) {
throw new IllegalArgumentException("defaultThemeId must be blank when no themes are declared");
}
} else {
defaultThemeId = requireText(defaultThemeId, "defaultThemeId");
if (themes.isEmpty()) {
throw new IllegalArgumentException("themes must not be empty");
}
if (themes.stream().map(FrontendVisualThemeSpec::themeId).noneMatch(defaultThemeId::equals)) {
throw new IllegalArgumentException("defaultThemeId must reference one of the declared themes");
}
}
}
public static FrontendSemanticPresentationSpec empty() {
return new FrontendSemanticPresentationSpec(List.of(), List.of());
return new FrontendSemanticPresentationSpec(List.of(), List.of(), "");
}
public FrontendVisualThemeSpec defaultTheme() {
if (themes.isEmpty()) {
throw new IllegalStateException("empty semantic presentation has no default theme");
}
return themes.stream()
.filter(theme -> theme.themeId().equals(defaultThemeId))
.findFirst()
.orElseThrow(() -> new IllegalStateException("defaultThemeId must resolve to a declared theme"));
}
public boolean isEmpty() {
return semanticKeys.isEmpty() && themes.isEmpty() && defaultThemeId.isEmpty();
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -0,0 +1,26 @@
package p.studio.compiler.models;
import java.util.Objects;
public record FrontendTokenStyleSpec(
String semanticKey,
String foreground,
boolean italic,
boolean bold,
boolean underline) {
public FrontendTokenStyleSpec {
semanticKey = requireText(semanticKey, "semanticKey");
foreground = requireText(foreground, "foreground");
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}

View File

@ -0,0 +1,47 @@
package p.studio.compiler.models;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record FrontendVisualThemeSpec(
String themeId,
String displayName,
FrontendEditorPaletteSpec editorPalette,
List<FrontendTokenStyleSpec> tokenStyles) {
public FrontendVisualThemeSpec {
themeId = requireText(themeId, "themeId");
displayName = requireText(displayName, "displayName");
editorPalette = Objects.requireNonNull(editorPalette, "editorPalette");
tokenStyles = List.copyOf(Objects.requireNonNull(tokenStyles, "tokenStyles"));
if (tokenStyles.isEmpty()) {
throw new IllegalArgumentException("tokenStyles must not be empty");
}
final Map<String, FrontendTokenStyleSpec> bySemanticKey = new LinkedHashMap<>();
for (final FrontendTokenStyleSpec tokenStyle : tokenStyles) {
final FrontendTokenStyleSpec previous = bySemanticKey.put(tokenStyle.semanticKey(), tokenStyle);
if (previous != null) {
throw new IllegalArgumentException("duplicate token style for semantic key: " + tokenStyle.semanticKey());
}
}
}
public FrontendTokenStyleSpec requireTokenStyle(final String semanticKey) {
return tokenStyles.stream()
.filter(tokenStyle -> tokenStyle.semanticKey().equals(semanticKey))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("missing token style for semantic key: " + semanticKey));
}
private static String requireText(
final String value,
final String field) {
final String candidate = Objects.requireNonNull(value, field).trim();
if (candidate.isEmpty()) {
throw new IllegalArgumentException(field + " must not be blank");
}
return candidate;
}
}