diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.check b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.check new file mode 100644 index 0000000000..f05da22ca7 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.check @@ -0,0 +1,25 @@ +package com.avaloq.tools.ddk.check.validation + +import com.avaloq.tools.ddk.check.testLanguage.Greeting + +catalog ExecutionEnvironment +for grammar com.avaloq.tools.ddk.check.TestLanguage { + + category DefaultCategory "Test category" { + + /** Checks the greeting name length */ + onSave error GreetingNameLength "Greeting name length" (String defaultName = "Franz") + message "Greeting name {0}" { + + for Greeting g { + if (g.name.length > 5) { + issue bind ("too long") data namelength (null) + } else if (g.name.equals(defaultName)) { + issue bind ("must not be Franz") data franzname (null) + } + } + + } + + } +} diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.html b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.html new file mode 100644 index 0000000000..ea4a27aaaa --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.html @@ -0,0 +1,168 @@ + + + + + + ExecutionEnvironment + + + +
+

ExecutionEnvironment

+ +
+
+
+

Test category

+

Checks the greeting name length

+
+
+

Greeting name length #

+ error +
+ Checks the greeting name length +
Greeting name ...
+
+
+
+ + diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.check b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.check new file mode 100644 index 0000000000..1ab022695a --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.check @@ -0,0 +1,99 @@ +package com.avaloq.tools.ddk.check.validation + +import com.avaloq.tools.ddk.check.lib.IResourceCache +import com.avaloq.tools.ddk.check.testLanguage.Greeting +import com.google.inject.Inject +import java.util.List +import com.google.common.collect.ImmutableList + +/** + * Check catalog for com.avaloq.tools.ddk.check.TestLanguage + */ +catalog LibraryChecks +for grammar com.avaloq.tools.ddk.check.TestLanguage { + + @Inject IResourceCache cache; + + + category InjectionChecks "Checks on injections in check catalogs." { + + /** Warning to indicate that this catalog is active. */ + live warning CheckCatalogIsActive "Check catalog is active" + message "Catalog is active" { + for Greeting { + issue; + } + } + + /** Error if the injection didn't work. */ + live error CacheInjectionFailed "Cache injection failed" + message "Cache was not injected" { + for Greeting g { + if (cache === null) { + issue; + } + } + } + + /** Error if values cannot be read from cache. */ + live error CacheDoesntWork "Cache doesn't work" + message "{0}" { + for Greeting { + val String key = this.qualifiedCatalogName + ".testValue"; + try { + cache.put(it, key, Boolean.TRUE); + val Boolean value = cache.get(it, key); + if (value === null || !value) { + issue bind ("Could not read value from cache: " + value); + } + } catch (Throwable t) { + issue bind ("Exception in cache access: " + t.getMessage()); + } + } + } + } + + category FormalParameterChecks "Checks on formal parameters" { + + /** Test formal parameter access. */ + live error FormalParameters "Formal Parameters" + (String param1 = "param1", boolean param2 = !!true, Boolean param3 = false, List names = #['foo', 'bar', 'ba\u0001\nz'], List ints = #[5, -42, 7]) + message "{0}" { + for Greeting { + val String p1 = param1; + val boolean p2 = false; + val List expectedNames = ImmutableList::of('foo', 'bar', 'ba\u0001\nz'); + val expectedInts = #[5, -42, 7]; + if (!"param1".equals(p1)) { + issue bind ('String parameter wrong (expected "param1"): ' + p1); + } + if (p2 != param3 || !p2 != param2) { + issue bind ("Boolean parameter wrong."); + } + var int i = 0; + val List names = names; // Whoa! + if (names.size != expectedNames.size) { + issue bind ("Expected three names, got " + names.size); + } else { + while (i < names.size) { + if (!expectedNames.get(i).equals(names.get(i))) { + issue bind ('String mismatch in list, expected "' + expectedNames.get(i) + '" but got "' + names.get(i) + '"') + } + i = i + 1; + } + } + val INTS = ints; + if (INTS.size != expectedInts.size) { + issue bind('Expected three ints, got ' + INTS.size); + } + i = 0; + while (i < INTS.size) { + if (expectedInts.get(i).intValue != INTS.get(i).intValue) { + issue bind ('Integer mismatch at index ' + i + ':' + expectedInts.get(i) + ' != ' + INTS.get(i)); + } + i = i + 1; + } + } + } + } +} diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.html b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.html new file mode 100644 index 0000000000..a84e860e46 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.html @@ -0,0 +1,198 @@ + + + + + + LibraryChecks + + + +
+

LibraryChecks

+

Check catalog for com.avaloq.tools.ddk.check.TestLanguage

+ +
+
+
+

Checks on injections in check catalogs.

+

Warning to indicate that this catalog is active.

+
+
+

Check catalog is active #

+ warning +
+ Warning to indicate that this catalog is active. +
Catalog is active
+
+
+
+

Cache injection failed #

+ error +
+ Error if the injection didn't work. +
Cache was not injected
+
+
+
+

Cache doesn't work #

+ error +
+ Error if values cannot be read from cache. +
...
+
+
+
+

Checks on formal parameters

+

Test formal parameter access.

+
+
+

Formal Parameters #

+ error +
+ Test formal parameter access. +
...
+
+
+
+ + diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-contexts.xml b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-contexts.xml new file mode 100644 index 0000000000..181dbbdfdc --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-contexts.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-toc.xml b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-toc.xml new file mode 100644 index 0000000000..b8d476bdb3 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-toc.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special.check b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special.check new file mode 100644 index 0000000000..41a5bbd608 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special.check @@ -0,0 +1,25 @@ +package com.avaloq.tools.ddk.check.validation + +import com.avaloq.tools.ddk.check.testLanguage.Greeting + +catalog Special +for grammar com.avaloq.tools.ddk.check.TestLanguage { + + /** A check outside any category. */ + live warning TopLevel "Naming & + + +
+

Check Catalogs

+

2 catalogs documented.

+
+
+ +
+ + diff --git a/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/toc.xml b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/toc.xml new file mode 100644 index 0000000000..1ac3dd2a3e --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/toc.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckDocGenerationTest.java b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckDocGenerationTest.java new file mode 100644 index 0000000000..fc939dcb12 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckDocGenerationTest.java @@ -0,0 +1,322 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ + +package com.avaloq.tools.ddk.check.core.test; + +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; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.xtext.resource.XtextResourceSet; +import org.eclipse.xtext.testing.InjectWith; +import org.eclipse.xtext.testing.extensions.InjectionExtension; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.io.TempDir; + +import com.avaloq.tools.ddk.check.CheckInjectorProvider; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.avaloq.tools.ddk.check.generator.CheckDocumentationTemplates; +import com.avaloq.tools.ddk.check.generator.CheckGenerator; +import com.avaloq.tools.ddk.check.standalone.CheckDocApplication; +import com.google.inject.Inject; +import com.google.inject.Injector; + + +/** + * Golden-file regression test for the Check documentation generators ({@link CheckGenerator#compileDoc(CheckCatalog)}, + * {@link CheckDocumentationTemplates}) and the headless {@link CheckDocApplication}. + * + *

The {@code ExecutionEnvironment.check} and {@code LibraryChecks.check} fixtures are copies of the catalogs in + * {@code com.avaloq.tools.ddk.check.test.runtime}, and the golden files are copies of its committed {@code docs/}: + * {@code toc.xml} and {@code contexts.xml} as written by the PDE-based IDE builder, the HTML pages and + * {@code index.html} as written by the headless application. Regenerating those docs with the {@code generateCheckDocs} + * profile must therefore leave them unchanged. {@code Special.check} covers catalog-level checks and escaping.

+ */ +@InjectWith(CheckInjectorProvider.class) +@ExtendWith(InjectionExtension.class) +@SuppressWarnings("nls") +public class CheckDocGenerationTest extends AbstractCheckTestCase { + + /** Folder (relative to this class' package) holding the input fixtures and golden snapshots. */ + private static final String FIXTURES = "docgen/"; + + private static final String EXECUTION_ENVIRONMENT = "ExecutionEnvironment"; + private static final String LIBRARY_CHECKS = "LibraryChecks"; + private static final String TOC = "toc.xml"; + private static final String CONTEXTS = "contexts.xml"; + private static final String INDEX = "index.html"; + private static final String NOTHING_WRITTEN = "nothing may be written on failure"; + + @Inject + private CheckGenerator generator; + + @Inject + private CheckDocumentationTemplates templates; + + @Inject + private Injector injector; + + @Override + protected Injector getInjector() { + return injector; + } + + /** + * The per-catalog HTML page emitted by {@link CheckGenerator#compileDoc(CheckCatalog)} must match + * the golden snapshot for each catalog. + */ + @Test + public void testCompileDocMatchesGolden() { + final XtextResourceSet resourceSet = injector.getInstance(XtextResourceSet.class); + final CheckCatalog executionEnvironment = parse(resourceSet, EXECUTION_ENVIRONMENT + ".check"); + final CheckCatalog libraryChecks = parse(resourceSet, LIBRARY_CHECKS + ".check"); + + assertGolden(EXECUTION_ENVIRONMENT + ".html", generator.compileDoc(executionEnvironment)); + assertGolden(LIBRARY_CHECKS + ".html", generator.compileDoc(libraryChecks)); + } + + /** + * The aggregated {@code toc.xml}, {@code contexts.xml} and {@code index.html} across both catalogs must match the + * golden snapshot, which for {@code toc.xml} and {@code contexts.xml} is the PDE builder's output. + */ + @Test + public void testCompileTocContextsAndIndexMatchGolden() { + final XtextResourceSet resourceSet = injector.getInstance(XtextResourceSet.class); + final List catalogs = List.of( + parse(resourceSet, EXECUTION_ENVIRONMENT + ".check"), + parse(resourceSet, LIBRARY_CHECKS + ".check")); + + assertGolden(TOC, templates.compileToc(catalogs)); + assertGolden(CONTEXTS, templates.compileContexts(catalogs)); + assertGolden(INDEX, templates.compileIndex(catalogs)); + } + + /** + * Catalog-level checks are nested under their catalog after its categories, and every attribute value and label is + * escaped, so labels containing XML or HTML markup still produce well-formed output. + */ + @Test + public void testCatalogLevelChecksAndEscaping() { + final CheckCatalog special = parse(injector.getInstance(XtextResourceSet.class), "Special.check"); + + assertGolden("Special-toc.xml", templates.compileToc(List.of(special))); + assertGolden("Special-contexts.xml", templates.compileContexts(List.of(special))); + final String page = generator.compileDoc(special).toString(); + assertTrue(page.contains("
"), page); + assertTrue(page.contains("

Naming & <style> "), page); + assertTrue(page.contains("

Tips & "tricks"

"), page); + } + + /** + * The headless application writes the whole documentation tree byte-identical to the golden snapshot, with LF line + * endings, and deletes the pages of catalogs that no longer exist. + * + * @param tempDir + * scratch directory for the sources and the documentation + * @throws IOException + * if the fixture tree cannot be created or read + */ + @Test + public void testGenerateWritesGoldenTree(@TempDir final Path tempDir) throws IOException { + final Path sourceDir = tempDir.resolve("src"); + final Path docsDir = tempDir.resolve("docs"); + final Path packageDir = sourceDir.resolve("pkg"); + copyFixture(EXECUTION_ENVIRONMENT + ".check", packageDir); + copyFixture(LIBRARY_CHECKS + ".check", packageDir); + final Path stale = touch(docsDir.resolve("content/Removed.html")); + + assertTrue(CheckDocApplication.generate(injector, sourceDir, docsDir), "generation must succeed"); + + assertBytes(EXECUTION_ENVIRONMENT + ".html", docsDir.resolve("content/" + EXECUTION_ENVIRONMENT + ".html")); + assertBytes(LIBRARY_CHECKS + ".html", docsDir.resolve("content/" + LIBRARY_CHECKS + ".html")); + assertBytes(TOC, docsDir.resolve(TOC)); + assertBytes(CONTEXTS, docsDir.resolve(CONTEXTS)); + assertBytes(INDEX, docsDir.resolve(INDEX)); + assertFalse(Files.exists(stale), "pages of removed catalogs must be deleted"); + } + + /** + * The headless application writes nothing and reports failure for a catalog with syntax errors or when no catalog + * is found. + * + * @param tempDir + * scratch directory for the sources and the documentation + * @throws IOException + * if the fixture tree cannot be created + */ + @Test + public void testGenerateFailsOnSyntaxErrorsAndMissingCatalogs(@TempDir final Path tempDir) throws IOException { + final Path sourceDir = tempDir.resolve("src"); + final Path docsDir = tempDir.resolve("docs"); + Files.createDirectories(sourceDir); + + assertFalse(CheckDocApplication.generate(injector, sourceDir, docsDir), "no catalogs must fail"); + Files.writeString(sourceDir.resolve("Broken.check"), "package p catalog {"); + assertFalse(CheckDocApplication.generate(injector, sourceDir, docsDir), "syntax errors must fail"); + assertFalse(Files.exists(docsDir), NOTHING_WRITTEN); + } + + /** + * The headless application writes nothing and reports failure when two catalogs in different packages share a + * simple name, since their pages would overwrite each other. + * + * @param tempDir + * scratch directory for the sources and the documentation + * @throws IOException + * if the fixture tree cannot be created + */ + @Test + public void testGenerateFailsOnCatalogsSharingAPage(@TempDir final Path tempDir) throws IOException { + final Path sourceDir = tempDir.resolve("src"); + final Path docsDir = tempDir.resolve("docs"); + final Path packageA = sourceDir.resolve("a"); + final Path packageB = sourceDir.resolve("b"); + Files.createDirectories(packageA); + Files.createDirectories(packageB); + final String fileName = "Same.check"; + Files.writeString(packageA.resolve(fileName), "package a catalog Same {}"); + Files.writeString(packageB.resolve(fileName), "package b catalog Same {}"); + + assertTrue(CheckDocApplication.generate(injector, packageA, tempDir.resolve("single")), "one catalog alone must succeed"); + assertFalse(CheckDocApplication.generate(injector, sourceDir, docsDir), "catalogs sharing a page must fail"); + assertFalse(Files.exists(docsDir), NOTHING_WRITTEN); + } + + /** + * The headless source walk skips {@code target} and dot-prefixed directories only below the source + * directory, so a checkout under a dot-directory (for example {@code ~/.jenkins/workspace}) is still found. + * + * @param tempDir + * scratch directory for the fake checkout + * @throws IOException + * if the fixture tree cannot be created + */ + @Test + public void testFindCheckFilesIgnoresAncestorsOfSourceDir(@TempDir final Path tempDir) throws IOException { + final Path sourceDir = tempDir.resolve(".jenkins/workspace/src"); + final Path kept = touch(sourceDir.resolve("Kept.check")); + final Path nested = touch(sourceDir.resolve("pkg/Nested.check")); + touch(sourceDir.resolve("target/Built.check")); + touch(sourceDir.resolve(".settings/Hidden.check")); + + assertEquals(Set.of(kept, nested), Set.copyOf(CheckDocApplication.findCheckFiles(sourceDir))); + } + + /** + * Parses a {@code .check} fixture from the {@link #FIXTURES} folder into a {@link CheckCatalog}. + * + * @param resourceSet + * the resource set to load into + * @param fileName + * the fixture file name (without folder prefix) + * @return the parsed catalog + */ + private CheckCatalog parse(final XtextResourceSet resourceSet, final String fileName) { + final Resource resource = resourceSet.createResource(URI.createURI(FIXTURES + fileName)); + try (InputStream in = getClass().getResourceAsStream(FIXTURES + fileName)) { + assertNotNull(in, "Missing fixture " + FIXTURES + fileName); + resource.load(in, null); + } catch (IOException e) { + throw new IllegalStateException("Could not load fixture " + fileName, e); + } + assertTrue(resource.getErrors().isEmpty(), () -> fileName + " has syntax errors: " + resource.getErrors()); + final CheckCatalog catalog = (CheckCatalog) resource.getContents().get(0); + assertNotNull(catalog, "Resource " + fileName + " should contain a CheckCatalog"); + return catalog; + } + + /** + * Asserts that the generated content equals the golden file, comparing with LF line endings because the generators + * use the platform line separator; {@link #assertBytes(String, Path)} checks the written files byte for byte. + * + * @param goldenFileName + * the golden file name (without folder prefix) + * @param actual + * the generated content + */ + private void assertGolden(final String goldenFileName, final CharSequence actual) { + final String expected = new String(readResource(FIXTURES + goldenFileName), StandardCharsets.UTF_8); + assertEquals(expected, actual.toString().replace("\r\n", "\n"), goldenFileName + " must match the golden snapshot"); + } + + /** + * Asserts that a written file is byte-identical to the golden file. + * + * @param goldenFileName + * the golden file name (without folder prefix) + * @param actual + * the written file + * @throws IOException + * if the written file cannot be read + */ + private void assertBytes(final String goldenFileName, final Path actual) throws IOException { + final String expected = new String(readResource(FIXTURES + goldenFileName), StandardCharsets.UTF_8); + assertEquals(expected, Files.readString(actual), actual + " must be byte-identical to the golden snapshot"); + } + + /** + * Copies a fixture into a directory. + * + * @param fileName + * the fixture file name (without folder prefix) + * @param targetDir + * the directory to copy into + * @throws IOException + * if the copy fails + */ + private void copyFixture(final String fileName, final Path targetDir) throws IOException { + Files.createDirectories(targetDir); + Files.write(targetDir.resolve(fileName), readResource(FIXTURES + fileName)); + } + + /** + * Reads a classpath resource (relative to this class). + * + * @param name + * the resource name + * @return the resource content + */ + private byte[] readResource(final String name) { + try (InputStream in = getClass().getResourceAsStream(name)) { + assertNotNull(in, "Missing resource " + name); + return in.readAllBytes(); + } catch (IOException e) { + throw new IllegalStateException("Could not read resource " + name, e); + } + } + + /** + * Creates an empty file and its parent directories. + * + * @param file + * the file to create + * @return {@code file} + * @throws IOException + * if the file cannot be created + */ + private static Path touch(final Path file) throws IOException { + Files.createDirectories(file.getParent()); + return Files.createFile(file); + } + +} diff --git a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java index b7072d2827..4f3744b6ec 100644 --- a/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java +++ b/com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/test/core/CheckCoreTestSuite.java @@ -18,6 +18,7 @@ import com.avaloq.tools.ddk.check.core.test.BugAig1314; import com.avaloq.tools.ddk.check.core.test.BugAig830; import com.avaloq.tools.ddk.check.core.test.BugDsl27; +import com.avaloq.tools.ddk.check.core.test.CheckDocGenerationTest; import com.avaloq.tools.ddk.check.core.test.CheckLineSeparatorBindingTest; import com.avaloq.tools.ddk.check.core.test.CheckScopingTest; import com.avaloq.tools.ddk.check.core.test.IssueCodeToLabelMapGenerationTest; @@ -44,6 +45,7 @@ CheckJavaValidatorUtilTest.class, IssueCodeToLabelMapGenerationTest.class, IssueExpressionGenerationTest.class, + CheckDocGenerationTest.class, ProjectBasedTests.class, BugAig1314.class, BugDsl27.class, diff --git a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF index 89b61b03ac..eba63e076b 100644 --- a/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF +++ b/com.avaloq.tools.ddk.check.core/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: com.avaloq.tools.ddk.check.core Bundle-SymbolicName: com.avaloq.tools.ddk.check.core;singleton:=true -Bundle-Version: 17.3.3.qualifier +Bundle-Version: 17.4.0.qualifier Bundle-Vendor: Avaloq Group AG Bundle-RequiredExecutionEnvironment: JavaSE-21 Bundle-ActivationPolicy: lazy @@ -27,7 +27,8 @@ Require-Bundle: org.eclipse.xtext, com.avaloq.tools.ddk.xtext, org.apache.commons.lang3, org.apache.commons.text, - org.objectweb.asm;resolution:=optional + org.objectweb.asm;resolution:=optional, + org.eclipse.equinox.app Export-Package: com.avaloq.tools.ddk.check, com.avaloq.tools.ddk.check.check, com.avaloq.tools.ddk.check.check.impl, @@ -44,6 +45,7 @@ Export-Package: com.avaloq.tools.ddk.check, com.avaloq.tools.ddk.check.scoping, com.avaloq.tools.ddk.check.serializer, com.avaloq.tools.ddk.check.services, + com.avaloq.tools.ddk.check.standalone, com.avaloq.tools.ddk.check.typing, com.avaloq.tools.ddk.check.util, com.avaloq.tools.ddk.check.validation diff --git a/com.avaloq.tools.ddk.check.core/plugin.xml b/com.avaloq.tools.ddk.check.core/plugin.xml index 7edc2a56d3..81f2b8b7cc 100644 --- a/com.avaloq.tools.ddk.check.core/plugin.xml +++ b/com.avaloq.tools.ddk.check.core/plugin.xml @@ -11,4 +11,12 @@ + + + + + + diff --git a/com.avaloq.tools.ddk.check.core/pom.xml b/com.avaloq.tools.ddk.check.core/pom.xml index 955fdb5090..9d1b1ee757 100644 --- a/com.avaloq.tools.ddk.check.core/pom.xml +++ b/com.avaloq.tools.ddk.check.core/pom.xml @@ -6,7 +6,7 @@ 18.0.1-SNAPSHOT ../ddk-parent - 17.3.3-SNAPSHOT + 17.4.0-SNAPSHOT com.avaloq.tools.ddk com.avaloq.tools.ddk.check.core eclipse-plugin diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java new file mode 100644 index 0000000000..6ff4c00063 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java @@ -0,0 +1,719 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.generator; + +import java.util.List; +import java.util.Objects; + +import org.eclipse.emf.ecore.EObject; +import org.eclipse.xtend2.lib.StringConcatenation; +import org.eclipse.xtext.EcoreUtil2; +import org.eclipse.xtext.xbase.lib.IterableExtensions; + +import com.avaloq.tools.ddk.check.check.Category; +import com.avaloq.tools.ddk.check.check.Check; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.google.inject.Inject; + +/** + * Emits the Eclipse-Help {@code toc.xml} and context-help {@code contexts.xml} + * files for a set of {@link CheckCatalog}s as plain XML strings, plus the + * standalone-browser {@code index.html} landing page. Also exposes the shared + * {@link #STYLE} block used by both the index and the per-catalog pages. + */ +@SuppressWarnings("nls") +// CHECKSTYLE:CONSTANTS-OFF +public class CheckDocumentationTemplates { + + private static final String TOC_LABEL = "Check Catalogs"; + private static final String TOC_ANCHOR = "../com.avaloq.tools.ddk.check.runtime.ui/toc.xml#checkdocumentation"; + private static final String DOCS_REF_PREFIX = "docs/content/"; + + /** + * Single-source-of-truth stylesheet used by the index page and every per-catalog + * HTML page. Inlined into the page {@code } so the output is self-contained + * and renders in any standard browser without external assets. + */ + public static final String STYLE = buildStyle(); + + @Inject + private CheckGeneratorNaming naming; + + @Inject + private CheckGeneratorExtensions extensions; + + /** + * Build the contents of {@code docs/toc.xml} aggregating every catalog in {@code catalogs}. + * + * @param catalogs + * the catalogs to aggregate + * @return the {@code toc.xml} contents + */ + public CharSequence compileToc(final Iterable catalogs) { + final List sorted = IterableExtensions.sortBy(catalogs, CheckCatalog::getName); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + for (final CheckCatalog catalog : sorted) { + builder.append(" "); + builder.newLine(); + for (final Category category : catalog.getCategories()) { + builder.append(" "); + builder.newLine(); + for (final Check check : category.getChecks()) { + appendCheckTopic(builder, " ", catalog, check); + } + builder.append(" "); + builder.newLine(); + } + for (final Check check : catalog.getChecks()) { + appendCheckTopic(builder, " ", catalog, check); + } + builder.append(" "); + builder.newLine(); + } + builder.append(""); + return builder; + } + + /** + * Appends the leaf {@code } element of a check to a {@code toc.xml} under construction. + * + * @param builder + * the {@code toc.xml} under construction + * @param indent + * the indentation of the element + * @param catalog + * the catalog documenting the check + * @param check + * the check + */ + private void appendCheckTopic(final StringConcatenation builder, final String indent, final CheckCatalog catalog, final Check check) { + builder.append(indent + ""); + builder.newLine(); + builder.append(indent + ""); + builder.newLine(); + } + + /** + * Build the contents of {@code docs/contexts.xml} aggregating every check across {@code catalogs}. + * + * @param catalogs + * the catalogs whose checks are aggregated + * @return the {@code contexts.xml} contents + */ + public CharSequence compileContexts(final Iterable catalogs) { + final List entries = IterableExtensions.sortBy( + IterableExtensions.map( + IterableExtensions.flatMap(catalogs, CheckCatalog::getAllChecks), + check -> new ContextEntry( + naming.getContextId(check), + check.getLabel(), + docRef(parentCatalog(check)) + "#" + naming.getContextId(check))), + entry -> entry.id); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + for (final ContextEntry e : entries) { + builder.append(" "); + builder.newLine(); + builder.append(" "); + builder.newLine(); + builder.append(" "); + builder.newLine(); + } + builder.append(""); + return builder; + } + + /** + * Build the contents of {@code docs/index.html} listing every catalog with a link to its page. + * + * @param catalogs + * the catalogs to list + * @return the {@code index.html} contents + */ + public CharSequence compileIndex(final Iterable catalogs) { + final List sorted = IterableExtensions.sortBy(catalogs, CheckCatalog::getName); + StringConcatenation builder = new StringConcatenation(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("Check Catalogs"); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

Check Catalogs

"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(sorted.size(), " "); + builder.append(" catalog"); + if (sorted.size() != 1) { + builder.append("s"); + } + builder.append(" documented.

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append(""); + builder.newLine(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(""); + builder.newLine(); + builder.append(""); + builder.newLine(); + return builder; + } + + /** + * Reference used from {@code toc.xml}/{@code contexts.xml} (relative to {@code docs/}). + * + * @param c + * the catalog + * @return the documentation reference path + */ + private String docRef(final CheckCatalog c) { + return DOCS_REF_PREFIX + naming.docFileName(c); + } + + /** + * Returns the catalog containing the given object. + * + * @param o + * the contained object + * @return the enclosing catalog, or {@code null} if none + */ + private CheckCatalog parentCatalog(final EObject o) { + return EcoreUtil2.getContainerOfType(o, CheckCatalog.class); + } + + /** + * Path used from {@code index.html} (same directory as {@code content/}). + * + * @param c + * the catalog + * @return the relative reference path + */ + private String indexRef(final CheckCatalog c) { + return "content/" + naming.docFileName(c); + } + + /** + * Escape characters that have special meaning inside an XML attribute value. + * + * @param s + * the raw attribute value, may be {@code null} + * @return the escaped value, or {@code null} if {@code s} is {@code null} + */ + private static String attrEscape(final String s) { + if (s == null) { + return null; + } + return htmlEscape(s).replace("'", "'"); + } + + /** + * Escape characters that have special meaning in HTML text or a double-quoted HTML attribute value. + * + * @param s + * the raw text, may be {@code null} + * @return the escaped text, or {@code null} if {@code s} is {@code null} + */ + static String htmlEscape(final String s) { + if (s == null) { + return null; + } + return s.replace("&", "&") + .replace("\"", """) + .replace("<", "<") + .replace(">", ">"); + } + + /** + * Builds the {@link #STYLE} stylesheet, preserving the exact CSS layout and line breaks. + * + * @return the stylesheet text + */ + private static String buildStyle() { + StringConcatenation builder = new StringConcatenation(); + builder.append(":root {"); + builder.newLine(); + builder.append(" "); + builder.append("--bg: #ffffff;"); + builder.newLine(); + builder.append(" "); + builder.append("--text: #1f2328;"); + builder.newLine(); + builder.append(" "); + builder.append("--text-muted: #57606a;"); + builder.newLine(); + builder.append(" "); + builder.append("--border: #d0d7de;"); + builder.newLine(); + builder.append(" "); + builder.append("--card-bg: #f6f8fa;"); + builder.newLine(); + builder.append(" "); + builder.append("--code-bg: #f6f8fa;"); + builder.newLine(); + builder.append(" "); + builder.append("--link: #0969da;"); + builder.newLine(); + builder.append(" "); + builder.append("--accent: #218bff;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-error-bg: #ffe5e5; --sev-error-text: #82071e; --sev-error-border: #ffadb0;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-warning-bg: #fff8c5; --sev-warning-text: #633c01; --sev-warning-border: #f3df5b;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-info-bg: #ddf4ff; --sev-info-text: #054a72; --sev-info-border: #80ccff;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-ignore-bg: #eaeef2; --sev-ignore-text: #57606a; --sev-ignore-border: #d0d7de;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("@media (prefers-color-scheme: dark) {"); + builder.newLine(); + builder.append(" "); + builder.append(":root {"); + builder.newLine(); + builder.append(" "); + builder.append("--bg: #0d1117;"); + builder.newLine(); + builder.append(" "); + builder.append("--text: #e6edf3;"); + builder.newLine(); + builder.append(" "); + builder.append("--text-muted: #8d96a0;"); + builder.newLine(); + builder.append(" "); + builder.append("--border: #30363d;"); + builder.newLine(); + builder.append(" "); + builder.append("--card-bg: #161b22;"); + builder.newLine(); + builder.append(" "); + builder.append("--code-bg: #161b22;"); + builder.newLine(); + builder.append(" "); + builder.append("--link: #58a6ff;"); + builder.newLine(); + builder.append(" "); + builder.append("--accent: #1f6feb;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-error-bg: #3d1419; --sev-error-text: #ffa198; --sev-error-border: #6e1216;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-warning-bg: #3a2c00; --sev-warning-text: #f0d97c; --sev-warning-border: #7a5a00;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-info-bg: #0c2d4a; --sev-info-text: #79c0ff; --sev-info-border: #1f6feb;"); + builder.newLine(); + builder.append(" "); + builder.append("--sev-ignore-bg: #1c2128; --sev-ignore-text: #8d96a0; --sev-ignore-border: #30363d;"); + builder.newLine(); + builder.append(" "); + builder.append("}"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("* { box-sizing: border-box; }"); + builder.newLine(); + builder.append("body {"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 16px;"); + builder.newLine(); + builder.append(" "); + builder.append("line-height: 1.55;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text);"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--bg);"); + builder.newLine(); + builder.append(" "); + builder.append("max-width: 820px;"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0 auto;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 2rem 1.25rem 4rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("h1 { font-size: 1.875rem; margin: 0 0 0.5rem; }"); + builder.newLine(); + builder.append("h2 { font-size: 1.375rem; margin: 2rem 0 1rem; padding-bottom: 0.4rem; border-bottom: 1px solid var(--border); }"); + builder.newLine(); + builder.append("h3 { font-size: 1.05rem; margin: 0; }"); + builder.newLine(); + builder.append("p { margin: 0.5rem 0; }"); + builder.newLine(); + builder.append("a { color: var(--link); text-decoration: none; }"); + builder.newLine(); + builder.append("a:hover { text-decoration: underline; }"); + builder.newLine(); + builder.append("header.catalog-header { margin-bottom: 1.5rem; padding-bottom: 1rem; border-bottom: 1px solid var(--border); }"); + builder.newLine(); + builder.append("header.catalog-header p { color: var(--text-muted); }"); + builder.newLine(); + builder.append("nav.jump {"); + builder.newLine(); + builder.append(" "); + builder.append("margin-top: 1rem;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.75rem 1rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.9rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("nav.jump strong { display: block; margin-bottom: 0.4rem; font-size: 0.8rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--text-muted); }"); + builder.newLine(); + builder.append("nav.jump ul { margin: 0; padding-left: 1.25rem; }"); + builder.newLine(); + builder.append("nav.jump li { margin: 0.15rem 0; }"); + builder.newLine(); + builder.append("section.category { margin: 2rem 0; }"); + builder.newLine(); + builder.append("section.category > p { color: var(--text-muted); margin-bottom: 0.75rem; }"); + builder.newLine(); + builder.append("article.check {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.75rem 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.85rem 1.15rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check > header {"); + builder.newLine(); + builder.append(" "); + builder.append("display: flex;"); + builder.newLine(); + builder.append(" "); + builder.append("align-items: baseline;"); + builder.newLine(); + builder.append(" "); + builder.append("gap: 0.6rem;"); + builder.newLine(); + builder.append(" "); + builder.append("flex-wrap: wrap;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check a.anchor {"); + builder.newLine(); + builder.append(" "); + builder.append("visibility: hidden;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text-muted);"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 400;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.85em;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("article.check > header:hover a.anchor { visibility: visible; }"); + builder.newLine(); + builder.append("article.check:target {"); + builder.newLine(); + builder.append(" "); + builder.append("border-color: var(--accent);"); + builder.newLine(); + builder.append(" "); + builder.append("box-shadow: 0 0 0 3px rgba(33, 139, 255, 0.18);"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append(".severity {"); + builder.newLine(); + builder.append(" "); + builder.append("display: inline-block;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.1rem 0.6rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.72rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 600;"); + builder.newLine(); + builder.append(" "); + builder.append("text-transform: uppercase;"); + builder.newLine(); + builder.append(" "); + builder.append("letter-spacing: 0.05em;"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 999px;"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid transparent;"); + builder.newLine(); + builder.append(" "); + builder.append("white-space: nowrap;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append(".severity.sev-error { background: var(--sev-error-bg); color: var(--sev-error-text); border-color: var(--sev-error-border); }"); + builder.newLine(); + builder.append(".severity.sev-warning { background: var(--sev-warning-bg); color: var(--sev-warning-text); border-color: var(--sev-warning-border); }"); + builder.newLine(); + builder.append(".severity.sev-info { background: var(--sev-info-bg); color: var(--sev-info-text); border-color: var(--sev-info-border); }"); + builder.newLine(); + builder.append(".severity.sev-ignore { background: var(--sev-ignore-bg); color: var(--sev-ignore-text); border-color: var(--sev-ignore-border); }"); + builder.newLine(); + builder.append("pre.message {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.6rem 0 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 0.55rem 0.8rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--code-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.85rem;"); + builder.newLine(); + builder.append(" "); + builder.append("white-space: pre-wrap;"); + builder.newLine(); + builder.append(" "); + builder.append("word-break: break-word;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("pre.message::before {"); + builder.newLine(); + builder.append(" "); + builder.append("content: \"Message\";"); + builder.newLine(); + builder.append(" "); + builder.append("display: block;"); + builder.newLine(); + builder.append(" "); + builder.append("font-family: inherit;"); + builder.newLine(); + builder.append(" "); + builder.append("font-size: 0.68rem;"); + builder.newLine(); + builder.append(" "); + builder.append("font-weight: 600;"); + builder.newLine(); + builder.append(" "); + builder.append("text-transform: uppercase;"); + builder.newLine(); + builder.append(" "); + builder.append("letter-spacing: 0.05em;"); + builder.newLine(); + builder.append(" "); + builder.append("color: var(--text-muted);"); + builder.newLine(); + builder.append(" "); + builder.append("margin-bottom: 0.2rem;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("ul.catalog-list { list-style: none; padding: 0; margin: 1.5rem 0 0; }"); + builder.newLine(); + builder.append("ul.catalog-list li {"); + builder.newLine(); + builder.append(" "); + builder.append("margin: 0.75rem 0;"); + builder.newLine(); + builder.append(" "); + builder.append("padding: 1rem 1.25rem;"); + builder.newLine(); + builder.append(" "); + builder.append("background: var(--card-bg);"); + builder.newLine(); + builder.append(" "); + builder.append("border: 1px solid var(--border);"); + builder.newLine(); + builder.append(" "); + builder.append("border-radius: 6px;"); + builder.newLine(); + builder.append("}"); + builder.newLine(); + builder.append("ul.catalog-list li h2 { margin: 0; border-bottom: none; padding-bottom: 0; font-size: 1.2rem; }"); + builder.newLine(); + builder.append("ul.catalog-list li p { color: var(--text-muted); margin: 0.25rem 0 0; }"); + builder.newLine(); + return builder.toString(); + } + + /** Immutable record of a single context-help entry, sortable by id. */ + private static final class ContextEntry { + private final String id; + private final String label; + private final String href; + + /** + * Creates a context-help entry. + * + * @param id + * the context id + * @param label + * the display label + * @param href + * the documentation reference + */ + ContextEntry(final String id, final String label, final String href) { + this.id = id; + this.label = label; + this.href = href; + } + + @Override + public int hashCode() { + return Objects.hash(id, label, href); + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ContextEntry other = (ContextEntry) obj; + return Objects.equals(id, other.id) && Objects.equals(label, other.label) && Objects.equals(href, other.href); + } + + @Override + public String toString() { + return "ContextEntry [id=" + id + ", label=" + label + ", href=" + href + "]"; + } + } +// CHECKSTYLE:CONSTANTS-ON +} diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java index 252f47a9cc..ceeea79755 100644 --- a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckGenerator.java @@ -83,89 +83,142 @@ public void doGenerate(final Resource resource, final IFileSystemAccess fsa) { } // CHECKSTYLE:CONSTANTS-OFF the repeated literals are fragments of the emitted HTML/Java source, not nameable constants - /* Documentation compiler, generates HTML output. */ + /** + * Documentation compiler, generates a self-contained HTML page per catalog. + * + * @param catalog + * the catalog to document + * @return the HTML page contents + */ public CharSequence compileDoc(final CheckCatalog catalog) { StringConcatenation builder = new StringConcatenation(); - final CharSequence body = bodyDoc(catalog); - builder.newLineIfNotEmpty(); - builder.append(""); + builder.append(""); builder.newLine(); - builder.append(""); + builder.append(""); builder.newLine(); builder.append(""); builder.newLine(); builder.append(" "); - builder.append(""); + builder.append(""); builder.newLine(); builder.append(" "); - builder.append(""); + builder.append(""); builder.newLine(); builder.append(" "); builder.append(""); builder.append(catalog.getName(), " "); builder.append(""); builder.newLineIfNotEmpty(); - builder.append(""); + builder.append(" "); + builder.append(""); builder.newLine(); + builder.append(""); builder.newLine(); builder.append(""); builder.newLine(); builder.append(" "); - builder.append("

Check Catalog "); - builder.append(catalog.getName(), " "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(catalog.getName(), " "); builder.append("

"); builder.newLineIfNotEmpty(); - builder.append(" "); + builder.append(" "); final String formattedDescription = generatorExtensions.formatDescription(catalog.getDescription()); builder.newLineIfNotEmpty(); if (formattedDescription != null) { - builder.append(" "); + builder.append(" "); builder.append("

"); - builder.append(formattedDescription, " "); + builder.append(formattedDescription, " "); builder.append("

"); builder.newLineIfNotEmpty(); } + if (!catalog.getChecks().isEmpty() || !catalog.getCategories().isEmpty()) { + builder.append(" "); + builder.append(""); + builder.newLine(); + } + builder.append(" "); + builder.append("
"); + builder.newLine(); builder.append(" "); - builder.append(body, " "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append(bodyDoc(catalog), " "); builder.newLineIfNotEmpty(); - builder.append(""); + builder.append(" "); + builder.append("
"); builder.newLine(); + builder.append(""); builder.newLine(); builder.append(""); builder.newLine(); return builder; } + /** + * Renders the body of the documentation page: one article per check, grouped by category. + * + * @param catalog + * the catalog whose checks and categories are rendered + * @return the HTML body fragment + */ public CharSequence bodyDoc(final CheckCatalog catalog) { StringConcatenation builder = new StringConcatenation(); for (final Check check : catalog.getChecks()) { - builder.append("

"); - builder.append(check.getLabel()); - builder.append(" ("); - builder.append(check.getDefaultSeverity().name().toLowerCase()); - builder.append(")

"); - builder.newLineIfNotEmpty(); - final String formattedCheckDescription = generatorExtensions.formatDescription(check.getDescription()); - builder.newLineIfNotEmpty(); - if (formattedCheckDescription != null) { - builder.append(formattedCheckDescription); - builder.newLineIfNotEmpty(); - } - builder.append("

Message: "); - builder.append(generatorExtensions.replacePlaceholder(check.getMessage())); - builder.append("


"); + builder.append(checkArticle(check)); builder.newLineIfNotEmpty(); } for (final Category category : catalog.getCategories()) { - builder.append("
"); + builder.append("
"); builder.newLine(); builder.append(" "); builder.append("

"); - builder.append(category.getLabel(), " "); + builder.append(CheckDocumentationTemplates.htmlEscape(category.getLabel()), " "); builder.append("

"); builder.newLineIfNotEmpty(); builder.append(" "); @@ -173,49 +226,73 @@ public CharSequence bodyDoc(final CheckCatalog catalog) { builder.newLineIfNotEmpty(); if (formattedCategoryDescription != null) { builder.append(" "); + builder.append("

"); builder.append(formattedCategoryDescription, " "); + builder.append("

"); builder.newLineIfNotEmpty(); } for (final Check check : category.getChecks()) { builder.append(" "); - builder.append("
"); - builder.newLineIfNotEmpty(); - builder.append(" "); - builder.append(" "); - builder.append("

"); - builder.append(check.getLabel(), " "); - builder.append(" ("); - builder.append(check.getDefaultSeverity().name().toLowerCase(), " "); - builder.append(")

"); - builder.newLineIfNotEmpty(); - builder.append(" "); - builder.append(" "); - final String formattedCheckDescription = generatorExtensions.formatDescription(check.getDescription()); + builder.append(checkArticle(check), " "); builder.newLineIfNotEmpty(); - if (formattedCheckDescription != null) { - builder.append(" "); - builder.append(" "); - builder.append(formattedCheckDescription, " "); - builder.newLineIfNotEmpty(); - } - builder.append(" "); - builder.append(" "); - builder.append("

Message: "); - builder.append(generatorExtensions.replacePlaceholder(check.getMessage()), " "); - builder.append("

"); - builder.newLineIfNotEmpty(); - builder.append(" "); - builder.append("
"); - builder.newLine(); } - builder.append("
"); + builder.append(""); builder.newLine(); } return builder; } + /** + * Renders a single check as an HTML {@code
} block. + * + * @param check + * the check to render + * @return the HTML article fragment + */ + private CharSequence checkArticle(final Check check) { + StringConcatenation builder = new StringConcatenation(); + builder.append("
"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + builder.append("

"); + builder.append(CheckDocumentationTemplates.htmlEscape(check.getLabel()), " "); + builder.append(" #

"); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(""); + builder.append(check.getDefaultSeverity().name().toLowerCase(), " "); + builder.append(""); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append("
"); + builder.newLine(); + builder.append(" "); + final String formattedCheckDescription = generatorExtensions.formatDescription(check.getDescription()); + builder.newLineIfNotEmpty(); + if (formattedCheckDescription != null) { + builder.append(" "); + builder.append(formattedCheckDescription, " "); + builder.newLineIfNotEmpty(); + } + builder.append(" "); + builder.append("
");
+    builder.append(generatorExtensions.replacePlaceholder(check.getMessage()), "  ");
+    builder.append("
"); + builder.newLineIfNotEmpty(); + builder.append("
"); + builder.newLine(); + return builder; + } + /* * Creates an IssueCodes file for a Check Catalog. Every Check Catalog will have its own file * of issue codes. diff --git a/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java new file mode 100644 index 0000000000..3dcfc430d3 --- /dev/null +++ b/com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2026 Avaloq Group AG and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Avaloq Group AG - initial API and implementation + *******************************************************************************/ +package com.avaloq.tools.ddk.check.standalone; + +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; + +import org.eclipse.emf.common.util.URI; +import org.eclipse.emf.ecore.EObject; +import org.eclipse.emf.ecore.resource.Resource; +import org.eclipse.equinox.app.IApplication; +import org.eclipse.equinox.app.IApplicationContext; +import org.eclipse.xtext.resource.XtextResourceSet; + +import com.avaloq.tools.ddk.check.CheckStandaloneSetup; +import com.avaloq.tools.ddk.check.check.CheckCatalog; +import com.avaloq.tools.ddk.check.generator.CheckDocumentationTemplates; +import com.avaloq.tools.ddk.check.generator.CheckGenerator; +import com.avaloq.tools.ddk.check.generator.CheckGeneratorNaming; +import com.google.inject.Injector; + + +/** + * Eclipse application that emits the full Check documentation tree (HTML pages, an + * {@code index.html} landing page and the Eclipse-Help {@code toc.xml} / {@code contexts.xml}) + * from every {@code .check} file under a source directory, without requiring an Eclipse + * workbench. Unlike the IDE builder, it documents every catalog regardless of the + * project's Check generator preferences. + * + * Arguments (positional): {@code } where {@code docsDir} + * is the project's {@code docs/} folder. HTML pages land in + * {@code /content/.html}; the other files land directly in + * {@code /}. The application exits with {@code 1} and writes nothing if a + * catalog has syntax errors, two catalogs share a simple name (and hence a page), or + * none is found. + */ +@SuppressWarnings({"nls", "PMD.SystemPrintln"}) +public class CheckDocApplication implements IApplication { + + /** Unix line separator used for all written output, independent of the platform. */ + private static final String LF = "\n"; + + /** Exit code reported when no documentation was written. */ + private static final Integer EXIT_ERROR = 1; + + @Override + public Object start(final IApplicationContext context) throws IOException { + String[] args = (String[]) context.getArguments().get(IApplicationContext.APPLICATION_ARGS); + if (args == null || args.length < 2) { + System.err.println("Usage: -application com.avaloq.tools.ddk.check.core.docApplication "); + return EXIT_ERROR; + } + Injector injector = new CheckStandaloneSetup().createInjectorAndDoEMFRegistration(); + return generate(injector, Path.of(args[0]).toRealPath(), Path.of(args[1])) ? IApplication.EXIT_OK : EXIT_ERROR; + } + + /** + * Writes the documentation tree for every catalog under {@code sourceDir} into {@code docsDir}. Nothing is + * written if a {@code .check} file has syntax errors, two catalogs would share a page, or no catalog is found; + * pages of catalogs that no longer exist are deleted. + * + * @param injector + * the Check language injector + * @param sourceDir + * directory walked recursively for {@code .check} files + * @param docsDir + * the project's {@code docs/} folder + * @return {@code true} if the documentation was written, {@code false} if errors were reported instead + * @throws IOException + * if reading or writing fails + */ + public static boolean generate(final Injector injector, final Path sourceDir, final Path docsDir) throws IOException { + XtextResourceSet resourceSet = injector.getInstance(XtextResourceSet.class); + + List checkFiles = findCheckFiles(sourceDir); + List catalogs = new ArrayList<>(); + Map pageSources = new HashMap<>(); + CheckGeneratorNaming naming = injector.getInstance(CheckGeneratorNaming.class); + boolean hasErrors = false; + for (Path checkFile : checkFiles) { + Resource resource = resourceSet.getResource(URI.createFileURI(checkFile.toAbsolutePath().toString()), true); + for (Resource.Diagnostic error : resource.getErrors()) { + System.err.println(checkFile + ":" + error.getLine() + ": " + error.getMessage()); + hasErrors = true; + } + for (EObject root : resource.getContents()) { + if (root instanceof CheckCatalog catalog) { + catalogs.add(catalog); + Path previous = pageSources.putIfAbsent(naming.docFileName(catalog), checkFile); + if (previous != null) { + System.err.println(checkFile + ": catalog " + catalog.getName() + " has the same page name as the catalog in " + previous); + hasErrors = true; + } + } + } + } + if (hasErrors) { + return false; + } + if (catalogs.isEmpty()) { + System.err.println("No catalogs found under " + sourceDir); + return false; + } + + Path contentDir = docsDir.resolve("content"); + Files.createDirectories(contentDir); + CheckGenerator generator = injector.getInstance(CheckGenerator.class); + for (CheckCatalog catalog : catalogs) { + Path target = contentDir.resolve(naming.docFileName(catalog)); + writeLf(target, generator.compileDoc(catalog)); + System.out.println("Wrote " + target); + } + deleteStalePages(contentDir, pageSources.keySet()); + CheckDocumentationTemplates docTemplates = injector.getInstance(CheckDocumentationTemplates.class); + Path toc = docsDir.resolve("toc.xml"); + writeLf(toc, docTemplates.compileToc(catalogs)); + System.out.println("Wrote " + toc); + Path contexts = docsDir.resolve("contexts.xml"); + writeLf(contexts, docTemplates.compileContexts(catalogs)); + System.out.println("Wrote " + contexts); + Path index = docsDir.resolve("index.html"); + writeLf(index, docTemplates.compileIndex(catalogs)); + System.out.println("Wrote " + index); + + System.out.println("Processed " + checkFiles.size() + " .check files (" + catalogs.size() + " catalogs)"); + return true; + } + + /** + * Deletes the {@code .html} pages in {@code contentDir} that do not belong to a current catalog. + * + * @param contentDir + * the folder holding the per-catalog pages + * @param pages + * the file names of the pages just written + * @throws IOException + * if the folder cannot be listed or a page cannot be deleted + */ + private static void deleteStalePages(final Path contentDir, final Set pages) throws IOException { + List stale = new ArrayList<>(); + try (DirectoryStream htmlFiles = Files.newDirectoryStream(contentDir, "*.html")) { + for (Path page : htmlFiles) { + if (!pages.contains(contentDir.relativize(page).toString())) { + stale.add(page); + } + } + } + for (Path page : stale) { + Files.delete(page); + System.out.println("Deleted " + page); + } + } + + /** + * Collects the {@code .check} files under {@code sourceDir}, skipping {@code target} and dot-prefixed + * directories below it. Only the path relative to {@code sourceDir} is inspected, so the location of the + * checkout itself (for example under {@code ~/.jenkins}) does not matter. + * + * @param sourceDir + * directory to walk recursively + * @return the {@code .check} files found, in walk order + * @throws IOException + * if the directory cannot be walked + */ + public static List findCheckFiles(final Path sourceDir) throws IOException { + List checkFiles = new ArrayList<>(); + try (Stream walk = Files.walk(sourceDir)) { + walk.filter(p -> p.toString().endsWith(".check")) + .filter(p -> isUserSource(sourceDir.relativize(p))) + .forEach(checkFiles::add); + } + return checkFiles; + } + + /** + * Writes {@code content} to {@code target} with Unix ({@code \n}) line endings, regardless of the + * platform separator the {@link org.eclipse.xtend2.lib.StringConcatenation} default constructor picks + * up via {@code System.lineSeparator()}. This keeps headless output byte-identical to the in-IDE path + * (which emits LF through the Check runtime's line-separator binding) and to the committed snapshot on Windows. + * + * @param target + * file to write + * @param content + * generated content + * @throws IOException + * if writing fails + */ + private static void writeLf(final Path target, final CharSequence content) throws IOException { + Files.writeString(target, content.toString().replace("\r\n", LF).replace("\r", LF)); + } + + /** True iff the relative path {@code p} contains no segment named {@code target} or starting with a dot. */ + private static boolean isUserSource(final Path p) { + for (Path segment : p) { + String name = segment.toString(); + if ("target".equals(name) || name.startsWith(".")) { + return false; + } + } + return true; + } + + @Override + public void stop() { + // no-op + } +} diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html b/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html index 3439e97b51..ea4a27aaaa 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/content/ExecutionEnvironment.html @@ -1,22 +1,168 @@ - - + + - - + + ExecutionEnvironment + - -

Check Catalog ExecutionEnvironment

-
-

Test category

- Checks the greeting name length -
-

Greeting name length (error)

- Checks the greeting name length -

Message: Greeting name ...

-
-
+
+

ExecutionEnvironment

+ +
+
+
+

Test category

+

Checks the greeting name length

+
+
+

Greeting name length #

+ error +
+ Checks the greeting name length +
Greeting name ...
+
+
+
- diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html b/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html index bcf5eaa3b9..a84e860e46 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/content/LibraryChecks.html @@ -1,42 +1,198 @@ - - + + - - + + LibraryChecks + - -

Check Catalog LibraryChecks

-

Check catalog for com.avaloq.tools.ddk.check.TestLanguage

-
-

Checks on injections in check catalogs.

- Warning to indicate that this catalog is active. -
-

Check catalog is active (warning)

- Warning to indicate that this catalog is active. -

Message: Catalog is active

-
-
-

Cache injection failed (error)

- Error if the injection didn't work. -

Message: Cache was not injected

-
-
-

Cache doesn't work (error)

- Error if values cannot be read from cache. -

Message: ...

-
-
-
-

Checks on formal parameters

- Test formal parameter access. -
-

Formal Parameters (error)

- Test formal parameter access. -

Message: ...

-
-
+
+

LibraryChecks

+

Check catalog for com.avaloq.tools.ddk.check.TestLanguage

+ +
+
+
+

Checks on injections in check catalogs.

+

Warning to indicate that this catalog is active.

+
+
+

Check catalog is active #

+ warning +
+ Warning to indicate that this catalog is active. +
Catalog is active
+
+
+
+

Cache injection failed #

+ error +
+ Error if the injection didn't work. +
Cache was not injected
+
+
+
+

Cache doesn't work #

+ error +
+ Error if values cannot be read from cache. +
...
+
+
+
+

Checks on formal parameters

+

Test formal parameter access.

+
+
+

Formal Parameters #

+ error +
+ Test formal parameter access. +
...
+
+
+
- diff --git a/com.avaloq.tools.ddk.check.test.runtime/docs/index.html b/com.avaloq.tools.ddk.check.test.runtime/docs/index.html new file mode 100644 index 0000000000..0126fa7d42 --- /dev/null +++ b/com.avaloq.tools.ddk.check.test.runtime/docs/index.html @@ -0,0 +1,160 @@ + + + + + + Check Catalogs + + + +
+

Check Catalogs

+

2 catalogs documented.

+
+
+ +
+ + diff --git a/com.avaloq.tools.ddk.check.test.runtime/pom.xml b/com.avaloq.tools.ddk.check.test.runtime/pom.xml index 92efbd8cb5..1f653fb118 100644 --- a/com.avaloq.tools.ddk.check.test.runtime/pom.xml +++ b/com.avaloq.tools.ddk.check.test.runtime/pom.xml @@ -10,4 +10,92 @@ com.avaloq.tools.ddk.check.test.runtime eclipse-plugin - \ No newline at end of file + + + + + generateCheckDocs + + + com.avaloq.tools.ddk + ddk-repository + ${project.parent.version} + eclipse-repository + + + + + + org.eclipse.tycho + tycho-eclipse-plugin + ${tycho.version} + + + generate-check-docs + generate-resources + + eclipse-run + + + JavaSE-21 + + -application + com.avaloq.tools.ddk.check.core.docApplication + ${project.basedir}/src + ${project.basedir}/docs + + + + reactor-p2 + p2 + file:${project.basedir}/../ddk-repository/target/repository/ + + + eclipse-release + p2 + ${check.docgen.p2.releases} + + + eclipse-emf + p2 + ${check.docgen.p2.emf} + + + eclipse-xtext + p2 + ${check.docgen.p2.xtext} + + + eclipse-mwe + p2 + ${check.docgen.p2.mwe} + + + eclipse-orbit + p2 + ${check.docgen.p2.orbit} + + + + + com.avaloq.tools.ddk.check.core + eclipse-plugin + + + + + + + + + + + diff --git a/ddk-parent/pom.xml b/ddk-parent/pom.xml index 1464bd6e8c..81820d4cb0 100644 --- a/ddk-parent/pom.xml +++ b/ddk-parent/pom.xml @@ -63,6 +63,14 @@ https://dsldevkit.github.io/dsl-devkit/p2/releases/latest/ https://dsldevkit.github.io/dsl-devkit/p2/snapshots/latest/ + + + https://download.eclipse.org/releases/2026-06/ + https://download.eclipse.org/modeling/emf/emf/builds/release/2.39.0/ + https://download.eclipse.org/modeling/tmf/xtext/updates/releases/2.43.0/ + https://download.eclipse.org/modeling/emft/mwe/updates/releases/2.25.0/ + https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.40.0 diff --git a/docs/check-doc-generation.md b/docs/check-doc-generation.md new file mode 100644 index 0000000000..41ffddb43c --- /dev/null +++ b/docs/check-doc-generation.md @@ -0,0 +1,152 @@ +# Generating Check catalog documentation + +Generate browsable HTML docs for your Check catalogs by running a headless application against your bundle's `.check` sources. The application emits: + +- `docs/index.html` — landing page listing every catalog (self-contained, opens in any browser). +- `docs/content/.html` — one styled page per catalog with severity badges and deep-link anchors. +- `docs/toc.xml`, `docs/contexts.xml` — Eclipse Help integration artifacts (skip if you only need the browser pages). + +The styling is inline; no external CSS, JS or images. Dark mode follows the OS preference. + +## Regenerating these docs inside dsl-devkit + +If you are working in this repository and want to regenerate the committed snapshot under +`com.avaloq.tools.ddk.check.test.runtime/docs/`, run this single command from the reactor root: + +```bash +mvn -pl :com.avaloq.tools.ddk.check.test.runtime,:ddk-repository -am -PgenerateCheckDocs -DskipTests clean package +``` + +`check.core` is not published to an external p2 site during a local build, so the in-repo profile +resolves it from the **reactor** p2 site `ddk-repository/target/repository/`. That is why the command +also builds `:ddk-repository` (the `-am` / explicit module ensures it is built first) and why the +profile lists it first among its `eclipse-run` repositories. The steps below describe the flow for +**external** projects documenting their *own* catalogs, where `check.core` comes from the published p2 +site instead. + +Regenerating must leave the committed files unchanged. `CheckDocGenerationTest` (in +`com.avaloq.tools.ddk.check.core.test`) runs the application over copies of the two catalogs and compares +the result byte for byte with copies of the committed files, so generator drift fails the build. The +`toc.xml` and `contexts.xml` copies are the IDE builder's output, so both paths produce the same files. + +## Prerequisites + +A Tycho-based project with a target platform file. Plain-Maven projects are not supported — the generator runs inside an Equinox runtime started by `tycho-eclipse-plugin:eclipse-run`. + +## 1. Add the profile to the consumer pom + +In the bundle whose `.check` sources you want documented: + +```xml + + + generateCheckDocs + + + + org.eclipse.tycho + tycho-eclipse-plugin + + + generate-check-docs + generate-resources + eclipse-run + + JavaSE-21 + + -application + com.avaloq.tools.ddk.check.core.docApplication + ${project.basedir}/src + ${project.basedir}/docs + + + + ddk + p2 + https://dsldevkit.github.io/dsl-devkit/p2/releases/latest/ + + + eclipse-release + p2 + https://download.eclipse.org/releases/2026-06/ + + + eclipse-emf + p2 + https://download.eclipse.org/modeling/emf/emf/builds/release/2.39.0/ + + + eclipse-xtext + p2 + https://download.eclipse.org/modeling/tmf/xtext/updates/releases/2.43.0/ + + + eclipse-mwe + p2 + https://download.eclipse.org/modeling/emft/mwe/updates/releases/2.25.0/ + + + eclipse-orbit + p2 + https://download.eclipse.org/tools/orbit/simrel/orbit-aggregation/release/4.40.0 + + + + + com.avaloq.tools.ddk.check.core + eclipse-plugin + + + + + + + + + + +``` + +The application takes two args: `` (walked recursively for `*.check` files, skipping `target/` and +dot-prefixed directories) and `` (written into). It documents every catalog it finds, regardless of +the project's Check generator preferences, and deletes pages in `docs/content/` whose catalog no longer +exists. It fails the build without writing anything if a catalog has syntax errors, two catalogs in different packages +share a name (their pages would overwrite each other), or none is found. + +`eclipse-run` installs only what its `` provide; it does *not* read your build's target +platform. The DDK p2 site carries only the DDK bundles, so the Eclipse, EMF, Xtext, MWE and Orbit sites +are required too. Use the versions that match your DDK release: the `check.docgen.p2.*` properties in +`ddk-parent/pom.xml` at that release's tag list them. For snapshots, replace the DDK URL with +`https://dsldevkit.github.io/dsl-devkit/p2/snapshots/latest/`; to pin a version, use +`p2/releases//` or `p2/snapshots//`. + +## 2. Invoke + +```bash +mvn -PgenerateCheckDocs -DskipTests package +``` + +Then open `docs/index.html` in any browser. + +## 3. Register the Eclipse Help files (optional) + +Skip this if you only need the browser pages. Otherwise, register `toc.xml` and `contexts.xml` in the +bundle's `plugin.xml`: + +```xml + + + + + + +``` + +Add `docs/` to `bin.includes` in `build.properties`, and install `com.avaloq.tools.ddk.check.runtime.ui` +with your product: its table of contents provides the `checkdocumentation` anchor that `toc.xml` links to. + +## Troubleshooting + +- **"Cannot resolve ..." from `eclipse-run`** — one of the p2 sites above is missing from the execution's ``, or its version does not match your DDK release. +- **"No catalogs found under ..."** — `` contains no `.check` files outside `target/` and dot-prefixed directories. The application walks the directory recursively and fails the build in this case; check the path you passed as the first ``. +- **"Application com.avaloq.tools.ddk.check.core.docApplication could not be found"** — the bundle is missing from the `` list of the `eclipse-run` execution (it is *not* enough to have it on the target platform; `eclipse-run` only installs what you list).