From 22486fbafa8316bf1f1ca8a7d9592598d9a1f3b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 24 Sep 2026 23:02:09 +0200 Subject: [PATCH 1/4] feat(check): headless CheckDocApplication with PDE-free toc/contexts templates An Eclipse application (com.avaloq.tools.ddk.check.core.docApplication) walks a source directory for .check files and writes per-catalog HTML, an index page, and toc.xml/contexts.xml for Eclipse Help. CheckDocumentationTemplates emits the help files as plain strings in the PDE-based IDE builder's layout, without org.eclipse.pde.internal APIs. Output is written with LF line endings on every platform. The walk skips target/ and dot-prefixed directories below the source directory only. Syntax errors, two catalogs sharing a page name, or an empty source tree fail the application without writing anything; pages of removed catalogs are deleted. check.core moves to 17.4.0 for the new exported package. Co-Authored-By: Claude Opus 5.5 --- .../META-INF/MANIFEST.MF | 6 +- com.avaloq.tools.ddk.check.core/plugin.xml | 8 + com.avaloq.tools.ddk.check.core/pom.xml | 2 +- .../CheckDocumentationTemplates.java | 719 ++++++++++++++++++ .../check/standalone/CheckDocApplication.java | 223 ++++++ 5 files changed, 955 insertions(+), 3 deletions(-) create mode 100644 com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/generator/CheckDocumentationTemplates.java create mode 100644 com.avaloq.tools.ddk.check.core/src/com/avaloq/tools/ddk/check/standalone/CheckDocApplication.java 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(); + for (final CheckCatalog catalog : sorted) { + builder.append(" "); + builder.append("
  • "); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(" "); + builder.append("

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

    "); + builder.newLineIfNotEmpty(); + builder.append(" "); + builder.append(" "); + final String description = extensions.formatDescription(catalog.getDescription()); + builder.newLineIfNotEmpty(); + if (description != null) { + builder.append(" "); + builder.append(" "); + builder.append("

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

    "); + builder.newLineIfNotEmpty(); + } + 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/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 + } +} From 74958be708a3c5d6e27f8ef85c3237c2fee0b59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 24 Sep 2026 23:02:10 +0200 Subject: [PATCH 2/4] feat(check): restyle generated catalog docs as self-contained HTML compileDoc emits an HTML5 page with inline CSS, severity badges and anchors, and escapes labels and ids. It no longer references the check.css stylesheet in check.runtime.ui, which stays for pages generated by earlier releases. Co-Authored-By: Claude Opus 5.5 --- .../ddk/check/generator/CheckGenerator.java | 209 ++++++++++++------ 1 file changed, 143 insertions(+), 66 deletions(-) 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. From 667c3d367163cb5905c68fc8604ac39da0f6df50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 24 Sep 2026 23:02:10 +0200 Subject: [PATCH 3/4] test(check): golden-file regression test for the doc templates Compares the templates and the files written by the headless application with copies of check.test.runtime's docs, and covers catalog-level checks, escaping, the source filter and the failure cases. Co-Authored-By: Claude Opus 5.5 --- .../test/docgen/ExecutionEnvironment.check | 25 ++ .../test/docgen/ExecutionEnvironment.html | 168 +++++++++ .../core/test/docgen/LibraryChecks.check | 99 ++++++ .../check/core/test/docgen/LibraryChecks.html | 198 +++++++++++ .../core/test/docgen/Special-contexts.xml | 9 + .../check/core/test/docgen/Special-toc.xml | 11 + .../ddk/check/core/test/docgen/Special.check | 25 ++ .../ddk/check/core/test/docgen/contexts.xml | 18 + .../ddk/check/core/test/docgen/index.html | 160 +++++++++ .../tools/ddk/check/core/test/docgen/toc.xml | 23 ++ .../core/test/CheckDocGenerationTest.java | 322 ++++++++++++++++++ .../check/test/core/CheckCoreTestSuite.java | 2 + 12 files changed, 1060 insertions(+) create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.check create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/ExecutionEnvironment.html create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.check create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/LibraryChecks.html create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-contexts.xml create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special-toc.xml create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/Special.check create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/contexts.xml create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/index.html create mode 100644 com.avaloq.tools.ddk.check.core.test/resource/com/avaloq/tools/ddk/check/core/test/docgen/toc.xml create mode 100644 com.avaloq.tools.ddk.check.core.test/src/com/avaloq/tools/ddk/check/core/test/CheckDocGenerationTest.java 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, From 4ea19b98a11591a7edaccd80bfe9fc1a64b849a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Dinis=20Ferreira?= Date: Thu, 24 Sep 2026 23:02:11 +0200 Subject: [PATCH 4/4] build(check): generateCheckDocs profile, regenerated docs, consumer guide An opt-in generateCheckDocs profile in check.test.runtime runs the application through tycho-eclipse-plugin:eclipse-run; the regenerated docs snapshot is committed. docs/check-doc-generation.md covers the in-repo flow, documenting another project's catalogs, and Eclipse Help registration. Co-Authored-By: Claude Opus 5.5 --- .../docs/content/ExecutionEnvironment.html | 178 ++++++++++++-- .../docs/content/LibraryChecks.html | 228 +++++++++++++++--- .../docs/index.html | 160 ++++++++++++ .../pom.xml | 90 ++++++- ddk-parent/pom.xml | 8 + docs/check-doc-generation.md | 152 ++++++++++++ 6 files changed, 763 insertions(+), 53 deletions(-) create mode 100644 com.avaloq.tools.ddk.check.test.runtime/docs/index.html create mode 100644 docs/check-doc-generation.md 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).