diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java index 27b9781c..28ea25d7 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/main/java/p/studio/compiler/PBSDefinitions.java @@ -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(); } diff --git a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java index 761a4cff..c78457c0 100644 --- a/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java +++ b/prometeu-compiler/frontends/prometeu-frontend-pbs/src/test/java/p/studio/compiler/pbs/PbsSemanticPresentationContractTest.java @@ -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()); } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java new file mode 100644 index 00000000..3cd2cfd0 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendEditorPaletteSpec.java @@ -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; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java index 73691d86..989f5959 100644 --- a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendSemanticPresentationSpec.java @@ -5,14 +5,53 @@ import java.util.Objects; public record FrontendSemanticPresentationSpec( List semanticKeys, - List resources) { + List 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; } } diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java new file mode 100644 index 00000000..5405017c --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendTokenStyleSpec.java @@ -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; + } +} diff --git a/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java new file mode 100644 index 00000000..cf8603c2 --- /dev/null +++ b/prometeu-compiler/prometeu-compiler-core/src/main/java/p/studio/compiler/models/FrontendVisualThemeSpec.java @@ -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 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 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; + } +}