diff --git a/docs/_snippets/applies_to-version.md b/docs/_snippets/applies_to-version.md index f98f758d0..ef6ed62be 100644 --- a/docs/_snippets/applies_to-version.md +++ b/docs/_snippets/applies_to-version.md @@ -1,10 +1,65 @@ `applies_to` accepts the following version formats: -* `Major.Minor` -* `Major.Minor.Patch` +### Version specifiers -Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor.Patch` format. +You can use version specifiers to precisely control how versions are interpreted: + +| Specifier | Syntax | Description | Example | +|-----------|--------|-------------|---------| +| Greater than or equal (default) | `x.x` `x.x+` `x.x.x` `x.x.x+` | Feature available from this version onwards | `ga 9.2+` or `ga 9.2` | +| Range (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | Feature available only in this version range | `beta 9.0-9.1` | +| Exact version | `=x.x` `=x.x.x` | Feature available only in this specific version | `preview =9.0` | + +Regardless of the version format used in the source file, the version number is always rendered in the `Major.Minor` format in badges. + +:::{note} +The `+` suffix is optional for greater-than-or-equal syntax. Both `ga 9.2` and `ga 9.2+` have the same meaning. +::: + +### Examples + +```yaml +# Greater than or equal (feature available from 9.2 onwards) +stack: ga 9.2 +stack: ga 9.2+ + +# Range (feature was in beta from 9.0 to 9.1, then became GA) +stack: ga 9.2+, beta 9.0-9.1 + +# Exact version (feature was in preview only in 9.0) +stack: ga 9.1+, preview =9.0 +``` + +### Implicit version inference for multiple lifecycles {#implicit-version-inference} + +When you specify multiple lifecycles with simple versions (without explicit specifiers), the system automatically infers the version ranges: + +**Input:** +```yaml +stack: preview 9.0, alpha 9.1, beta 9.2, ga 9.4 +``` + +**Interpreted as:** +```yaml +stack: preview =9.0, alpha =9.1, beta 9.2-9.3, ga 9.4+ +``` + +The inference rules are: +1. **Consecutive versions**: If a lifecycle is immediately followed by another in the next minor version, it's treated as an **exact version** (`=x.x`). +2. **Non-consecutive versions**: If there's a gap between one lifecycle's version and the next lifecycle's version, it becomes a **range** from the start version to one version before the next lifecycle. +3. **Last lifecycle**: The highest versioned lifecycle is always treated as **greater-than-or-equal** (`x.x+`). + +This makes it easy to document features that evolve through multiple lifecycle stages. For example, a feature that goes through preview → beta → GA can be written simply as: + +```yaml +stack: preview 9.0, beta 9.1, ga 9.3 +``` + +Which is automatically interpreted as: +```yaml +stack: preview =9.0, beta 9.1-9.2, ga 9.3+ +``` :::{note} -**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 8.18.6, ga 9.1.2, ga 8.19.2, ga 9.0.6` will be displayed as `stack: ga 9.1.2, ga 9.0.6, ga 8.19.2, ga 8.18.6`. Items without versions (like `ga` without a version or `all`) are sorted last. +**Automatic Version Sorting**: When you specify multiple versions for the same product, the build system automatically sorts them in descending order (highest version first) regardless of the order you write them in the source file. For example, `stack: ga 9.1, beta 9.0, preview 8.18` will be displayed with the highest priority lifecycle and version first. Items without versions are sorted last. ::: \ No newline at end of file diff --git a/docs/syntax/_snippets/inline-level-applies-examples.md b/docs/syntax/_snippets/inline-level-applies-examples.md index 58f38476c..1fedf4759 100644 --- a/docs/syntax/_snippets/inline-level-applies-examples.md +++ b/docs/syntax/_snippets/inline-level-applies-examples.md @@ -55,7 +55,7 @@ This example shows how to use directly a key from the second level of the `appli ::::{tab-item} Output - {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0` -- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0` +- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0` - {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0` :::: @@ -63,7 +63,7 @@ This example shows how to use directly a key from the second level of the `appli ::::{tab-item} Markdown ```markdown - {applies_to}`serverless: ga` {applies_to}`stack: ga 9.1.0` -- {applies_to}`edot_python: preview 1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta 1.0.0, ga 1.2.0` +- {applies_to}`edot_python: preview =1.7.0, ga 1.8.0` {applies_to}`apm_agent_java: beta =1.0.0, ga 1.2.0` - {applies_to}`stack: ga 9.0` {applies_to}`eck: ga 3.0` ``` :::: diff --git a/docs/syntax/_snippets/multiple-lifecycle-states.md b/docs/syntax/_snippets/multiple-lifecycle-states.md index bb0bedf40..8c9dcd069 100644 --- a/docs/syntax/_snippets/multiple-lifecycle-states.md +++ b/docs/syntax/_snippets/multiple-lifecycle-states.md @@ -1,12 +1,35 @@ -`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions. For example: +`applies_to` keys accept comma-separated values to specify lifecycle states for multiple product versions. -* A feature is added in 9.1 as tech preview and becomes GA in 9.4: +When you specify multiple lifecycles with simple versions, the system automatically infers whether each version represents an exact version, a range, or an open-ended range. Refer to [Implicit version inference](/_snippets/applies_to-version.md#implicit-version-inference) for details. + +### Examples + +* A feature is added in 9.0 as tech preview and becomes GA in 9.1: + + ```yml + applies_to: + stack: preview 9.0, ga 9.1 + ``` + + The preview is automatically interpreted as `=9.0` (exact), and GA as `9.1+` (open-ended). + +* A feature goes through multiple stages before becoming GA: + + ```yml + applies_to: + stack: preview 9.0, beta 9.1, ga 9.3 + ``` + + Interpreted as: `preview =9.0`, `beta 9.1-9.2`, `ga 9.3+` + +* A feature is unavailable for one version, beta for another, preview for a range, then GA: ```yml applies_to: - stack: preview 9.1, ga 9.4 + stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4 ``` + Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3`, `ga 9.4+` * A feature is deprecated in ECE 4.0 and is removed in 4.8. At the same time, it has already been removed in {{ech}}: @@ -15,4 +38,17 @@ deployment: ece: deprecated 4.0, removed 4.8 ess: removed + ``` + + The deprecated lifecycle is interpreted as `4.0-4.7` (range until removal). + +* Use explicit specifiers when you need precise control: + + ```yml + applies_to: + # Explicit exact version + stack: preview =9.0, ga 9.1+ + + # Explicit range + stack: beta 9.0-9.1, ga 9.2+ ``` \ No newline at end of file diff --git a/docs/syntax/_snippets/versioned-lifecycle.md b/docs/syntax/_snippets/versioned-lifecycle.md index ae6af6fee..cfe372e69 100644 --- a/docs/syntax/_snippets/versioned-lifecycle.md +++ b/docs/syntax/_snippets/versioned-lifecycle.md @@ -7,6 +7,8 @@ --- ``` + This means the feature is available from version 9.3 onwards (equivalent to `ga 9.3+`). + * When a change is introduced as preview or beta, use `preview` or `beta` as value for the corresponding key within the `applies_to`: ``` @@ -16,6 +18,28 @@ --- ``` +* When a feature is available only in a specific version range, use the range syntax: + + ``` + --- + applies_to: + stack: beta 9.0-9.1, ga 9.2 + --- + ``` + + This means the feature was in beta from 9.0 to 9.1, then became GA in 9.2+. + +* When a feature was in a specific lifecycle for exactly one version, use the exact syntax: + + ``` + --- + applies_to: + stack: preview =9.0, ga 9.1 + --- + ``` + + This means the feature was in preview only in 9.0, then became GA in 9.1+. + * When a change introduces a deprecation, use `deprecated` as value for the corresponding key within the `applies_to`: ``` @@ -33,4 +57,6 @@ applies_to: stack: deprecated 9.1, removed 9.4 --- - ``` \ No newline at end of file + ``` + + With the implicit version inference, this is interpreted as `deprecated 9.1-9.3, removed 9.4+`. \ No newline at end of file diff --git a/docs/syntax/applies.md b/docs/syntax/applies.md index c6621e483..a1b990865 100644 --- a/docs/syntax/applies.md +++ b/docs/syntax/applies.md @@ -29,6 +29,41 @@ Where: - The lifecycle is mandatory. - The version is optional. +### Version Syntax + +Versions can be specified using several formats to indicate different applicability scenarios: + +| Description | Syntax | Example | Badge Display | +|:------------|:-------|:--------|:--------------| +| **Greater than or equal to** (default) | `x.x+` `x.x` `x.x.x+` `x.x.x` | `ga 9.1` or `ga 9.1+` | `9.1+` | +| **Range** (inclusive) | `x.x-y.y` `x.x.x-y.y.y` | `preview 9.0-9.2` | `9.0-9.2` or `9.0+`* | +| **Exact version** | `=x.x` `=x.x.x` | `beta =9.1` | `9.1` | + +\* Range display depends on release status of the second version. + +**Important notes:** + +- Versions are always displayed as **Major.Minor** (e.g., `9.1`) in badges, regardless of whether you specify patch versions in the source. +- Each version statement corresponds to the **latest patch** of the specified minor version (e.g., `9.1` represents 9.1.0, 9.1.1, 9.1.6, etc.). +- When critical patch-level differences exist, use plain text descriptions alongside the badge rather than specifying patch versions. + +### Version Validation Rules + +The build process enforces the following validation rules: + +- **One version per lifecycle**: Each lifecycle (GA, Preview, Beta, etc.) can only have one version declaration. + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2, ga 9.3` +- **One "greater than" per key**: Only one lifecycle per product key can use the `+` (greater than or equal to) syntax. + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2+, beta 9.0+` +- **Valid range order**: In ranges, the first version must be less than or equal to the second version. + - ✅ `stack: preview 9.0-9.2` + - ❌ `stack: preview 9.2-9.0` +- **No version overlaps**: Versions for the same key cannot overlap (ranges are inclusive). + - ✅ `stack: ga 9.2+, beta 9.0-9.1` + - ❌ `stack: ga 9.2+, beta 9.0-9.2` + ### Page level Page level annotations are added in the YAML frontmatter, starting with the `applies_to` key and following the [key-value reference](#key-value-reference). For example: @@ -134,6 +169,22 @@ Use the following key-value reference to find the appropriate key and value for ## Examples +### Version Syntax Examples + +The following table demonstrates the various version syntax options and their rendered output: + +| Source Syntax | Description | Badge Display | Notes | +|:-------------|:------------|:--------------|:------| +| `stack: ga 9.1` | Greater than or equal to 9.1 | `Stack│9.1+` | Default behavior, equivalent to `9.1+` | +| `stack: ga 9.1+` | Explicit greater than or equal to | `Stack│9.1+` | Explicit `+` syntax | +| `stack: preview 9.0-9.2` | Range from 9.0 to 9.2 (inclusive) | `Stack│Preview 9.0-9.2` | Shows range if 9.2.0 is released | +| `stack: preview 9.0-9.3` | Range where end is unreleased | `Stack│Preview 9.0+` | Shows `+` if 9.3.0 is not released | +| `stack: beta =9.1` | Exact version 9.1 only | `Stack│Beta 9.1` | No `+` symbol for exact versions | +| `stack: ga 9.2+, beta 9.0-9.1` | Multiple lifecycles | `Stack│9.2+` | Only highest priority lifecycle shown | +| `stack: ga 9.3, beta 9.1+` | Unreleased GA with Preview | `Stack│Beta 9.1+` | Shows Beta when GA unreleased with 2+ lifecycles | +| `serverless: ga` | No version (base 99999) | `Serverless` | No version badge for unversioned products | +| `deployment:`
` ece: ga 9.0+` | Nested deployment syntax | `ECE│9.0+` | Deployment products shown separately | + ### Versioning examples Versioned products require a `version` tag to be used with the `lifecycle` tag: @@ -240,22 +291,46 @@ applies_to: ## Look and feel +### Version Syntax Demonstrations + +:::::{dropdown} New version syntax examples + +The following examples demonstrate the new version syntax capabilities: + +**Greater than or equal to:** +- {applies_to}`stack: ga 9.1` (implicit `+`) +- {applies_to}`stack: ga 9.1+` (explicit `+`) +- {applies_to}`stack: preview 9.0+` + +**Ranges:** +- {applies_to}`stack: preview 9.0-9.2` (range display when both released) +- {applies_to}`stack: beta 9.1-9.3` (converts to `+` if end unreleased) + +**Exact versions:** +- {applies_to}`stack: beta =9.1` (no `+` symbol) +- {applies_to}`stack: deprecated =9.0` + +**Multiple lifecycles:** +- {applies_to}`stack: ga 9.2+, beta 9.0-9.1` (shows highest priority) + +::::: + ### Block :::::{dropdown} Block examples ```{applies_to} -stack: preview 9.1 +stack: preview 9.1+ serverless: ga -apm_agent_dotnet: ga 1.0.0 -apm_agent_java: beta 1.0.0 -edot_dotnet: preview 1.0.0 +apm_agent_dotnet: ga 1.0+ +apm_agent_java: beta 1.0+ +edot_dotnet: preview 1.0+ edot_python: -edot_node: ga 1.0.0 -elasticsearch: preview 9.0.0 -security: removed 9.0.0 -observability: deprecated 9.0.0 +edot_node: ga 1.0+ +elasticsearch: preview 9.0+ +security: removed 9.0 +observability: deprecated 9.0+ ``` ::::: @@ -331,4 +406,207 @@ Within the ProductApplicability category, EDOT and APM Agent items are sorted al :::{note} Inline applies annotations are rendered in the order they appear in the source file. -::: \ No newline at end of file +::: + +## Rulesets + +Badges and applicabilities are displayed according to pre-defined rules according to the release status, the amount of lifecycles declared in the `applies_to` statement, and the versions involved in the comparison when applicable. + +### Badges + +:::::{dropdown} No version declared (Serverless) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | – | `{product}` | +| Preview | – | – | `{product}\|Preview` | +| Beta | – | – | `{product} |Beta` | +| Deprecated | – | – | `{product} |Deprecated` | +| Removed | – | – | `{product} |Removed` | +| Unavailable | – | – | `{product} |Unavailable` | + +::::: + +:::::{dropdown} No version declared (Other versioning systems) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | – | `{product} |{base}+` | +| Preview | – | – | `{product}\|Preview {base}+` | +| Beta | – | – | `{product} |Beta {base}+` | +| Deprecated | – | – | `{product} |Deprecated {base}+` | +| Removed | – | – | `{product} |Removed {base}+` | +| Unavailable | – | – | `{product} |Unavailable {base}+` | + +::::: + +:::::{dropdown} Greater than or equal to "x.x" (x.x+, x.x, x.x.x+, x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `{product} |x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | Released | \>= 1 | `{product}\|Preview x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | Released | \>= 1 | `{product} |Beta x.x+` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | Released | \>= 1 | `{product} |Deprecated x.x+` | +| | Unreleased | 1 | `{product} |Deprecation planned` | +| | | \>= 2 | Use previous lifecycle | +| Removed | Released | \>= 1 | `{product} |Removed x.x` | +| | Unreleased | 1 | `{product} |Removal planned` | +| | | \>= 2 | Use previous lifecycle | + +::::: + +:::::{dropdown} Range of "x.x-y.y" (x.x-y.y, x.x.x-y.y.y) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | `y.y.y` is released | \>= 1 | `{product} |x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | `y.y.y` is released | \>= 1 | `{product}\|Preview x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product}\|Preview x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | `y.y.y` is released | \>= 1 | `{product} |Beta x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Beta x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | `y.y.y` is released | \>= 1 | `{product} |Deprecated x.x-y.y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Deprecated x.x+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `{product} |Deprecation planned` | +| Removed | `y.y.y` is released | \>= 1 | `{product} |Removed x.x` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Removed x.x` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `{product} |Removal planned` | +| Unavailable | `y.y.y` is released | \>= 1 | `{product} |Unavailable X.X-Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `{product} |Unavailable X.X+` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | ??? | + +::::: + +:::::{dropdown} Exactly "x.x" (=x.x, =x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `{product} |X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Preview | Released | \>= 1 | `{product}\|Preview X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Beta | Released | \>= 1 | `{product} |Beta X.X` | +| | Unreleased | 1 | `{product}\|Planned` | +| | | \>= 2 | Use previous lifecycle | +| Deprecated | Released | \>= 1 | `{product} |Deprecated X.X` | +| | Unreleased | \>= 1 | `{product} |Deprecation planned` | +| Removed | Released | \>= 1 | `{product} |Removed X.X` | +| | Unreleased | \>=1 | `{product} |Removal planned` | +| Unavailable | Released | \>= 1 | `{product} |Unavailable X.X` | +| | Unreleased | \>= 1 | ??? | + +::::: + +### Headers for dynamic content on popover (Applicability list) + +:::::{dropdown} No version declared (Serverless) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | 1 | `Generally available` | +| Preview | – | 1 | `Preview` | +| Beta | – | 1 | `Beta` | +| Deprecated | – | 1 | `Deprecated` | +| Removed | – | 1 | `Removed` | +| Unavailable | – | 1 | `Unavailable` | +::::: + +:::::{dropdown} No version declared (Other versioning systems) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | – | 1 | `Generally available since {base}` | +| Preview | – | 1 | `Preview since {base}` | +| Beta | – | 1 | `Beta since {base}` | +| Deprecated | – | 1 | `Deprecated since {base}` | +| Removed | – | 1 | `Removed in {base}` | +| Unavailable | – | 1 | `Unavailable since {base}` | +::::: + +:::::{dropdown} Greater than or equal to "x.x" (x.x+, x.x, x.x.x+, x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `Generally available since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | Released | \>= 1 | `Preview since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | Released | \>= 1 | `Beta since X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | Released | \>= 1 | `Deprecated since X.X` | +| | Unreleased | \>= 1 | `Planned for deprecation` | +| Removed | Released | \>= 1 | `Removed in X.X` | +| | Unreleased | \>=1 | `Planned for removal` | +| Unavailable | Released | \>= 1 | `Unavailable since X.X` | +| | Unreleased | 1 | `Unavailable` | +| | | \>= 2 | Do not add to availability list | +::::: + +:::::{dropdown} Range of "x.x-y.y" (x.x-y.y, x.x.x-y.y.y) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | `y.y.y` is released | \>= 1 | `Generally available from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Generally available since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | `y.y.y` is released | \>= 1 | `Preview from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Preview since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | `y.y.y` is released | \>= 1 | `Beta from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Beta since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | `y.y.y` is released | \>= 1 | `Deprecated from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Deprecated since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `Planned for deprecation` | +| Removed | `y.y.y` is released | \>= 1 | `Removed in X.X` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Removed in X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | `Planned for removal` | +| Unavailable | `y.y.y` is released | \>= 1 | `Unavailable from X.X to Y.Y` | +| | `y.y.y` is **not** released `x.x.x` is released | \>= 1 | `Unavailable since X.X` | +| | `y.y.y` is **not** released `x.x.x` is **not** released | \>= 1 | Do not add to availability list | + + +::::: + +:::::{dropdown} Exactly "x.x" (=x.x, =x.x.x) + +| Lifecycle | Release status | Lifecycle count | Rendered output | +|:------------|:--------------------------------------------------------|-----------------|:-----------------------------| +| GA | Released | \>= 1 | `Generally available in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Preview | Released | \>= 1 | `Preview in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Beta | Released | \>= 1 | `Beta in X.X` | +| | Unreleased | 1 | `Planned` | +| | | \>= 2 | Do not add to availability list | +| Deprecated | Released | \>= 1 | `Deprecated in X.X` | +| | Unreleased | \>= 1 | `Planned for deprecation` | +| Removed | Released | \>= 1 | `Removed in X.X` | +| | Unreleased | \>=1 | `Planned for removal` | +| Unavailable | Released | \>= 1 | `Unavailable in X.X` | +| | Unreleased | \>= 1 | Do not add to availability list | + +::::: diff --git a/docs/testing/req.md b/docs/testing/req.md index 95907215f..a9cd202d0 100644 --- a/docs/testing/req.md +++ b/docs/testing/req.md @@ -8,24 +8,109 @@ mapped_pages: --- # Requirements +This page demonstrates various `applies_to` version syntax examples. + +## Version specifier examples + +### Greater than or equal (default) + +```{applies_to} +stack: ga 9.0 +``` + +This is equivalent to `ga 9.0+` — the feature is available from version 9.0 onwards. + +### Explicit range + +```{applies_to} +stack: beta 9.0-9.1, ga 9.2 +``` + +The feature was in beta from 9.0 to 9.1 (inclusive), then became GA in 9.2+. + +### Exact version + +```{applies_to} +stack: preview =9.0, ga 9.1 +``` + +The feature was in preview only in version 9.0 (exactly), then became GA in 9.1+. + +## Implicit version inference examples + +### Simple two-stage lifecycle + ```{applies_to} stack: preview 9.0, ga 9.1 ``` -1. Select **Create** to create a new policy, or select **Edit** {icon}`pencil` to open an existing policy. -1. Select **Create** to create a new policy, or select **Edit** {icon}`logo_vulnerability_management` to open an existing policy. +Interpreted as: `preview =9.0` (exact), `ga 9.1+` (open-ended). +### Multi-stage lifecycle with consecutive versions -{applies_to}`stack: preview 9.0` This tutorial is based on Elasticsearch 9.0. -This tutorial is based on Elasticsearch 9.0. This tutorial is based on Elasticsearch 9.0. -This tutorial is based on Elasticsearch 9.0. +```{applies_to} +stack: preview 9.0, beta 9.1, ga 9.2 +``` -what +Interpreted as: `preview =9.0`, `beta =9.1`, `ga 9.2+`. +### Multi-stage lifecycle with gaps -To follow this tutorial you will need to install the following components: +```{applies_to} +stack: unavailable 9.0, beta 9.1, preview 9.2, ga 9.4 +``` + +Interpreted as: `unavailable =9.0`, `beta =9.1`, `preview 9.2-9.3` (range to fill the gap), `ga 9.4+`. + +### Three stages with varying gaps + +```{applies_to} +stack: preview 8.0, beta 9.1, ga 9.3 +``` + +Interpreted as: `preview 8.0-8.19`, `beta 9.0-9.1`, `ga 9.2+`. + +## Inline examples +{applies_to}`stack: preview 9.0` This feature is in preview in 9.0. +{applies_to}`stack: beta 9.0-9.1` This feature was in beta from 9.0 to 9.1. + +{applies_to}`stack: ga 9.2+` This feature is generally available since 9.2. + +{applies_to}`stack: preview =9.0` This feature was in preview only in 9.0 (exact). + +## Deprecation and removal examples + +```{applies_to} +stack: deprecated 9.2, removed 9.5 +``` + +Interpreted as: `deprecated 9.2-9.4`, `removed 9.5+`. + +{applies_to}`stack: deprecated 9.0` This feature is deprecated starting in 9.0. + +{applies_to}`stack: removed 9.2` This feature was removed in 9.2. + +## Mixed deployment examples + +```{applies_to} +stack: ga 9.0 +deployment: + ece: ga 4.0 + eck: beta 3.0, ga 3.1 +``` + +### Handling multiple future versions + +```{applies_to} +eck: beta 3.4, ga 3.5, deprecated 3.9 +``` + + +## Additional content + +To follow this tutorial you will need to install the following components: - An installation of Elasticsearch, based on our hosted [Elastic Cloud](https://www.elastic.co/cloud) service (which includes a free trial period), or a self-hosted service that you run on your own computer. See the Install Elasticsearch section above for installation instructions. - A [Python](https://python.org) interpreter. Make sure it is a recent version, such as Python 3.8 or newer. @@ -36,5 +121,20 @@ The tutorial assumes that you have no previous knowledge of Elasticsearch or gen - The [Flask](https://flask.palletsprojects.com/) web framework for Python. - The command prompt or terminal application in your operating system. - {applies_to}`ece: removed` + +{applies_to}`ece: ` + +{applies_to}`stack: deprecated 7.16.0, removed 8.0.0` + +{applies_to}`ess: ` + +{applies_to}`stack: preview 9.0, ga 9.2, deprecated 9.7` + +{applies_to}`stack: preview 9.0, removed 9.1` + +{applies_to}`stack: preview 9.0.0-9.0.3, removed 9.3` + +{applies_to}`stack: preview 9.0, ga 9.4, removed 9.7` + +{applies_to}`stack: preview 9.0, deprecated 9.4, removed 9.7` diff --git a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs index b2c82a1ec..d94f5ca7c 100644 --- a/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs +++ b/src/Elastic.ApiExplorer/Elasticsearch/OpenApiDocumentExporter.cs @@ -209,7 +209,7 @@ private bool ShouldIncludeOperation(OpenApiOperation operation, string product) return true; // Could not parse version, safe to include // Get current version for the product - var versioningSystemId = product == "elasticsearch" + var versioningSystemId = product.Equals("elasticsearch", StringComparison.OrdinalIgnoreCase) ? VersioningSystemId.Stack : VersioningSystemId.Stack; // Both use Stack for now @@ -294,14 +294,14 @@ private static ProductLifecycle ParseLifecycle(string stateValue) /// /// Parses the version from "Added in X.Y.Z" pattern in the x-state string. /// - private static SemVersion? ParseVersion(string stateValue) + private static VersionSpec? ParseVersion(string stateValue) { var match = AddedInVersionRegex().Match(stateValue); if (!match.Success) return null; var versionString = match.Groups[1].Value; - return SemVersion.TryParse(versionString, out var version) ? version : null; + return VersionSpec.TryParse(versionString, out var version) ? version : null; } /// diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs index 3a4ab61f0..de122351f 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionConfiguration.cs @@ -151,4 +151,6 @@ public record VersioningSystem [YamlMember(Alias = "current")] public required SemVersion Current { get; init; } + + public bool IsVersioned() => Base.Major != AllVersions.Instance.Major; } diff --git a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs index 102dca32f..08259d8ae 100644 --- a/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs +++ b/src/Elastic.Documentation.Configuration/Versions/VersionInference.cs @@ -96,7 +96,7 @@ public class NoopVersionInferrer : IVersionInferrerService public VersioningSystem InferVersion(string repositoryName, IReadOnlyCollection? legacyPages, IReadOnlyCollection? products, ApplicableTo? applicableTo) => new() { Id = VersioningSystemId.Stack, - Base = new SemVersion(0, 0, 0), - Current = new SemVersion(0, 0, 0) + Base = ZeroVersion.Instance, + Current = ZeroVersion.Instance }; } diff --git a/src/Elastic.Documentation.Site/Assets/main.ts b/src/Elastic.Documentation.Site/Assets/main.ts index 38f9fcfd6..0beab1d5e 100644 --- a/src/Elastic.Documentation.Site/Assets/main.ts +++ b/src/Elastic.Documentation.Site/Assets/main.ts @@ -2,7 +2,6 @@ import { initAppliesSwitch } from './applies-switch' import { initCopyButton } from './copybutton' import { initHighlight } from './hljs' import { initImageCarousel } from './image-carousel' -import './markdown/applies-to' import { openDetailsWithAnchor } from './open-details-with-anchor' import { initNav } from './pages-nav' import { initSmoothScroll } from './smooth-scroll' @@ -33,6 +32,7 @@ initializeOtel({ // Parcel will automatically code-split this into a separate chunk import('./web-components/SearchOrAskAi/SearchOrAskAi') import('./web-components/VersionDropdown') +import('./web-components/AppliesToPopover') const { getOS } = new UAParser() const isLazyLoadNavigationEnabled = diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css index d371819df..1e74fec30 100644 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css +++ b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.css @@ -4,14 +4,58 @@ @apply text-subdued; - [data-tippy-content]:not([data-tippy-content='']) { - @apply cursor-help; + applies-to-popover { + display: contents; } .applicable-info { @apply border-grey-20 inline-flex cursor-default rounded-full border-[1px] bg-white pt-1.5 pr-3 pb-1.5 pl-3; } + .applicable-info--clickable { + /* Desktop: tooltip-like behavior, no pointer cursor */ + @apply cursor-default; + + &:hover { + @apply border-grey-30 bg-grey-10; + } + + &:focus { + outline: none; + } + + /* Desktop: no focus-visible outline for tooltip behavior */ + @media (hover: hover) and (pointer: fine) { + &:focus-visible { + outline: none; + } + } + + /* Mobile/touch: show focus outline for accessibility */ + @media (hover: none), (pointer: coarse) { + @apply cursor-pointer; + + &:focus-visible { + outline: 2px solid var(--color-blue-elastic); + outline-offset: 2px; + } + } + } + + /* Desktop: no pinned state styling */ + @media (hover: hover) and (pointer: fine) { + .applicable-info--pinned { + @apply border-grey-20 bg-white; + } + } + + /* Mobile/touch: show pinned state */ + @media (hover: none), (pointer: coarse) { + .applicable-info--pinned { + @apply border-blue-elastic bg-grey-10; + } + } + .applicable-meta { @apply inline-flex gap-1.5; } @@ -35,6 +79,12 @@ .applies.applies-inline { display: inline-block; vertical-align: bottom; + + applies-to-popover { + display: inline-flex; + vertical-align: bottom; + } + .applicable-separator { margin-left: calc(var(--spacing) * 1.5); margin-right: calc(var(--spacing) * 1.5); @@ -57,19 +107,9 @@ } } -.tippy-box[data-theme~='applies-to'] { - .tippy-content { - white-space: normal; - - strong { - display: block; - margin-bottom: calc(var(--spacing) * 1); - } - } - - .tippy-content > div:not(:last-child) { - border-bottom: 1px dotted var(--color-grey-50); - padding-bottom: calc(var(--spacing) * 3); - margin-bottom: calc(var(--spacing) * 3); - } +.euiPopover__panel { + /* Shadow and border for the popover */ + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1) !important; + border: 1px solid var(--color-grey-20) !important; + border-radius: 6px !important; } diff --git a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts b/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts deleted file mode 100644 index df5ad9d66..000000000 --- a/src/Elastic.Documentation.Site/Assets/markdown/applies-to.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { $$ } from 'select-dom' -import tippy from 'tippy.js' - -document.addEventListener('htmx:load', function () { - const selector = [ - '.applies [data-tippy-content]:not([data-tippy-content=""])', - '.applies-inline [data-tippy-content]:not([data-tippy-content=""])', - ].join(', ') - - const appliesToBadgesWithTooltip = $$(selector) - appliesToBadgesWithTooltip.forEach((badge) => { - const content = badge.getAttribute('data-tippy-content') - if (!content) return - tippy(badge, { - content, - allowHTML: true, - delay: [400, 100], - hideOnClick: false, - ignoreAttributes: true, - theme: 'applies-to', - }) - }) -}) diff --git a/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx new file mode 100644 index 000000000..c5918c61a --- /dev/null +++ b/src/Elastic.Documentation.Site/Assets/web-components/AppliesToPopover.tsx @@ -0,0 +1,493 @@ +'use strict' + +import '../eui-icons-cache' +import { EuiPopover, useGeneratedHtmlId } from '@elastic/eui' +import { css } from '@emotion/react' +import r2wc from '@r2wc/react-to-web-component' +import * as React from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' + +type PopoverAvailabilityItem = { + text: string + lifecycleDescription?: string +} + +type PopoverData = { + productDescription?: string + availabilityItems: PopoverAvailabilityItem[] + additionalInfo?: string + showVersionNote: boolean + versionNote?: string +} + +type AppliesToPopoverProps = { + badgeKey?: string + badgeLifecycleText?: string + badgeVersion?: string + lifecycleClass?: string + lifecycleName?: string + showLifecycleName?: boolean + showVersion?: boolean + hasMultipleLifecycles?: boolean + popoverData?: PopoverData + showPopover?: boolean + isInline?: boolean +} + +const AppliesToPopover = ({ + badgeKey, + badgeLifecycleText, + badgeVersion, + lifecycleClass, + lifecycleName, + showLifecycleName, + showVersion, + hasMultipleLifecycles, + popoverData, + showPopover = true, + isInline = false, +}: AppliesToPopoverProps) => { + const [isOpen, setIsOpen] = useState(false) + const [isPinned, setIsPinned] = useState(false) + const [openItems, setOpenItems] = useState>(new Set()) + const [isTouchDevice, setIsTouchDevice] = useState(false) + const popoverId = useGeneratedHtmlId({ prefix: 'appliesToPopover' }) + const contentRef = useRef(null) + const badgeRef = useRef(null) + const hoverTimeoutRef = useRef | null>(null) + + // Detect touch device on mount + useEffect(() => { + const checkTouchDevice = () => { + const hasCoarsePointer = + window.matchMedia('(pointer: coarse)').matches + const hasNoHover = window.matchMedia('(hover: none)').matches + setIsTouchDevice(hasCoarsePointer || hasNoHover) + } + checkTouchDevice() + // Re-check on resize in case device mode changes (e.g., dev tools toggle) + window.addEventListener('resize', checkTouchDevice) + return () => window.removeEventListener('resize', checkTouchDevice) + }, []) + + const hasPopoverContent = + popoverData && + (popoverData.productDescription || + popoverData.availabilityItems.length > 0 || + popoverData.additionalInfo || + popoverData.showVersionNote) + + const openPopover = useCallback(() => { + if (showPopover && hasPopoverContent) { + setIsOpen(true) + } + }, [showPopover, hasPopoverContent]) + + const closePopover = useCallback(() => { + if (!isPinned) { + setIsOpen(false) + } + }, [isPinned]) + + const handleClick = useCallback(() => { + // Only allow click/pin behavior on touch devices + // On desktop, the popover is tooltip-like (hover only) + if (!isTouchDevice) return + + if (showPopover && hasPopoverContent) { + if (isPinned) { + // If already pinned, unpin and close + setIsPinned(false) + setIsOpen(false) + } else { + // Pin the popover open + setIsPinned(true) + setIsOpen(true) + } + } + }, [showPopover, hasPopoverContent, isPinned, isTouchDevice]) + + const toggleItem = useCallback((index: number, e: React.MouseEvent) => { + e.stopPropagation() + setOpenItems((prev) => { + const next = new Set(prev) + if (next.has(index)) { + next.delete(index) + } else { + next.add(index) + } + return next + }) + }, []) + + const handleClosePopover = useCallback(() => { + setIsPinned(false) + setIsOpen(false) + }, []) + + const handleMouseEnter = useCallback(() => { + // Clear any pending close timeout + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + hoverTimeoutRef.current = null + } + openPopover() + }, [openPopover]) + + const handleMouseLeave = useCallback(() => { + // Small delay before closing to allow moving to the popover content + hoverTimeoutRef.current = setTimeout(() => { + closePopover() + }, 100) + }, [closePopover]) + + // Cleanup timeout on unmount + useEffect(() => { + return () => { + if (hoverTimeoutRef.current) { + clearTimeout(hoverTimeoutRef.current) + } + } + }, []) + + // Close popover when badge becomes hidden (e.g., parent details element collapses) + useEffect(() => { + if (!badgeRef.current || !isOpen) return + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + // If badge is no longer visible, close the popover + if (!entry.isIntersecting) { + setIsPinned(false) + setIsOpen(false) + } + }) + }, + { threshold: 0 } + ) + + observer.observe(badgeRef.current) + + return () => { + observer.disconnect() + } + }, [isOpen]) + + // Reset open items when popover closes + useEffect(() => { + if (!isOpen) { + setOpenItems(new Set()) + } + }, [isOpen]) + + const showSeparator = + badgeKey && (showLifecycleName || showVersion || badgeLifecycleText) + + // Only show interactive attributes on touch devices + const isInteractive = showPopover && hasPopoverContent && isTouchDevice + + const badgeButton = ( + { + if (isInteractive && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault() + handleClick() + } + }} + > + {badgeKey} + + {showSeparator && } + + + {showLifecycleName && ( + + {lifecycleName} + + )} + {showVersion ? ( + + {badgeVersion} + + ) : ( + badgeLifecycleText + )} + {hasMultipleLifecycles && ( + + + + + + )} + + + ) + + if (!showPopover || !hasPopoverContent) { + return badgeButton + } + + const renderAvailabilityItem = ( + item: PopoverAvailabilityItem, + index: number + ) => { + const isItemOpen = openItems.has(index) + + if (item.lifecycleDescription) { + return ( +
+
toggleItem(index, e)} + css={css` + display: flex; + align-items: center; + cursor: pointer; + padding: 4px 0; + color: var(--color-blue-elastic, #0077cc); + font-weight: 500; + + &::before { + content: ''; + display: inline-block; + width: 6px; + height: 6px; + border-right: 2px solid currentColor; + border-bottom: 2px solid currentColor; + transform: ${isItemOpen + ? 'rotate(45deg)' + : 'rotate(-45deg)'}; + margin-right: 8px; + transition: transform 0.15s ease; + } + + &:hover { + color: var(--color-blue-hover, #005fa3); + } + `} + > + + {item.text} + +
+ {isItemOpen && ( +

+ {item.lifecycleDescription} +

+ )} +
+ ) + } + + // Simple item without collapsible content + return ( +

+ {item.text} +

+ ) + } + + return ( + +
+ {/* Product description */} + {popoverData?.productDescription && ( +

+ )} + + {/* Availability section */} + {popoverData && popoverData.availabilityItems.length > 0 && ( + <> +

+ + Availability + +

+

+ {popoverData.availabilityItems.map((item, index) => + renderAvailabilityItem(item, index) + )} + + )} + + {/* Additional availability info */} + {popoverData?.additionalInfo && ( +

+ {popoverData.additionalInfo} +

+ )} + + {/* Version note */} + {popoverData?.showVersionNote && popoverData?.versionNote && ( +

+ + ⓘ + + {popoverData.versionNote} +

+ )} +
+
+ ) +} + +customElements.define( + 'applies-to-popover', + r2wc(AppliesToPopover, { + props: { + badgeKey: 'string', + badgeLifecycleText: 'string', + badgeVersion: 'string', + lifecycleClass: 'string', + lifecycleName: 'string', + showLifecycleName: 'boolean', + showVersion: 'boolean', + hasMultipleLifecycles: 'boolean', + popoverData: 'json', + showPopover: 'boolean', + isInline: 'boolean', + }, + }) +) + +export default AppliesToPopover diff --git a/src/Elastic.Documentation/AppliesTo/Applicability.cs b/src/Elastic.Documentation/AppliesTo/Applicability.cs index b91fdb991..2164a485e 100644 --- a/src/Elastic.Documentation/AppliesTo/Applicability.cs +++ b/src/Elastic.Documentation/AppliesTo/Applicability.cs @@ -37,13 +37,88 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics if (applications.Count == 0) return false; + // Infer version semantics when multiple items have GreaterThanOrEqual versions + applications = InferVersionSemantics(applications); + // Sort by version in descending order (the highest version first) - // Items without versions (AllVersions.Instance) are sorted last + // Items without versions (AllVersionsSpec.Instance) are sorted last var sortedApplications = applications.OrderDescending().ToArray(); availability = new AppliesCollection(sortedApplications); return true; } + /// + /// Infers versioning semantics according to the following ruleset: + /// - The highest version keeps GreaterThanOrEqual (e.g., 9.4+) + /// - Lower versions become Exact if consecutive, or Range to fill gaps + /// - This rule only applies when all versions are at minor level (patch = 0). + /// + private static List InferVersionSemantics(List applications) + { + // Get items with actual GreaterThanOrEqual versions (not AllVersionsSpec, not null, not ranges/exact) + var gteItems = applications + .Where(a => a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual } + && a.Version != AllVersionsSpec.Instance) + .ToList(); + + // If 0 or 1 GTE items, no inference needed + if (gteItems.Count <= 1) + return applications; + + // Only apply inference when all entries are on patch version 0 + if (gteItems.Any(a => a.Version!.Min.Patch != 0)) + return applications; + + // Sort GTE items by version ascending to process from lowest to highest + var sortedGteVersions = gteItems + .Select(a => a.Version!.Min) + .Distinct() + .OrderBy(v => v) + .ToList(); + + if (sortedGteVersions.Count <= 1) + return applications; + + var versionMapping = new Dictionary(); + + for (var i = 0; i < sortedGteVersions.Count; i++) + { + var currentVersion = sortedGteVersions[i]; + + if (i == sortedGteVersions.Count - 1) + { + // Highest version keeps GreaterThanOrEqual + versionMapping[currentVersion] = VersionSpec.GreaterThanOrEqual(currentVersion); + } + else + { + var nextVersion = sortedGteVersions[i + 1]; + + // Define an Exact or Range VersionSpec according to the numeric difference between lifecycles + if (currentVersion.Major == nextVersion.Major + && nextVersion.Minor == currentVersion.Minor + 1) + versionMapping[currentVersion] = VersionSpec.Exact(currentVersion); + else + { + var rangeEnd = new SemVersion(nextVersion.Major, nextVersion.Minor == 0 ? nextVersion.Minor : nextVersion.Minor - 1, 0); + versionMapping[currentVersion] = VersionSpec.Range(currentVersion, rangeEnd); + } + } + } + + // Apply the mapping to create updated applications + return applications.Select(a => + { + if (a.Version is null or AllVersionsSpec || a is not { Version.Kind: VersionSpecKind.GreaterThanOrEqual }) + return a; + + if (versionMapping.TryGetValue(a.Version.Min, out var newSpec)) + return a with { Version = newSpec }; + + return a; + }).ToList(); + } + public virtual bool Equals(AppliesCollection? other) { if ((object)this == other) @@ -98,36 +173,24 @@ public override string ToString() public record Applicability : IComparable, IComparable { public ProductLifecycle Lifecycle { get; init; } - public SemVersion? Version { get; init; } + public VersionSpec? Version { get; init; } public static Applicability GenerallyAvailable { get; } = new() { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = AllVersions.Instance + Version = AllVersionsSpec.Instance }; public string GetLifeCycleName() => - Lifecycle switch - { - ProductLifecycle.TechnicalPreview => "Preview", - ProductLifecycle.Beta => "Beta", - ProductLifecycle.Development => "Development", - ProductLifecycle.Deprecated => "Deprecated", - ProductLifecycle.Planned => "Planned", - ProductLifecycle.Discontinued => "Discontinued", - ProductLifecycle.Unavailable => "Unavailable", - ProductLifecycle.GenerallyAvailable => "GA", - ProductLifecycle.Removed => "Removed", - _ => throw new ArgumentOutOfRangeException(nameof(Lifecycle), Lifecycle, null) - }; + ProductLifecycleInfo.GetShortName(Lifecycle); /// public int CompareTo(Applicability? other) { - var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersions.Instance); - var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersions.Instance); + var xIsNonVersioned = Version is null || ReferenceEquals(Version, AllVersionsSpec.Instance); + var yIsNonVersioned = other?.Version is null || ReferenceEquals(other.Version, AllVersionsSpec.Instance); if (xIsNonVersioned && yIsNonVersioned) return 0; @@ -158,7 +221,7 @@ public override string ToString() _ => throw new ArgumentOutOfRangeException() }; _ = sb.Append(lifecycle); - if (Version is not null && Version != AllVersions.Instance) + if (Version is not null && Version != AllVersionsSpec.Instance) _ = sb.Append(' ').Append(Version); return sb.ToString(); } @@ -224,10 +287,10 @@ public static bool TryParse(string? value, IList<(Severity, string)> diagnostics ? null : tokens[1] switch { - null => AllVersions.Instance, - "all" => AllVersions.Instance, - "" => AllVersions.Instance, - var t => SemVersionConverter.TryParse(t, out var v) ? v : null + null => AllVersionsSpec.Instance, + "all" => AllVersionsSpec.Instance, + "" => AllVersionsSpec.Instance, + var t => VersionSpec.TryParse(t, out var v) ? v : null }; availability = new Applicability { Version = version, Lifecycle = lifecycle }; return true; diff --git a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs index cb881fbf6..eb4a0f7f3 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicabilitySelector.cs @@ -15,44 +15,30 @@ public static class ApplicabilitySelector /// The collection of applicabilities to select from /// The current version to use for comparison /// The most relevant applicability for display - public static Applicability GetPrimaryApplicability(IEnumerable applicabilities, SemVersion currentVersion) + public static Applicability GetPrimaryApplicability(IReadOnlyCollection applicabilities, SemVersion currentVersion) { - var applicabilityList = applicabilities.ToList(); - var lifecycleOrder = new Dictionary - { - [ProductLifecycle.GenerallyAvailable] = 0, - [ProductLifecycle.Beta] = 1, - [ProductLifecycle.TechnicalPreview] = 2, - [ProductLifecycle.Planned] = 3, - [ProductLifecycle.Deprecated] = 4, - [ProductLifecycle.Removed] = 5, - [ProductLifecycle.Unavailable] = 6 - }; - - var availableApplicabilities = applicabilityList - .Where(a => a.Version is null || a.Version is AllVersions || a.Version <= currentVersion) - .ToList(); + var availableApplicabilities = applicabilities + .Where(a => a.Version is null || a.Version is AllVersionsSpec || a.Version.Min <= currentVersion).ToArray(); - if (availableApplicabilities.Count != 0) + if (availableApplicabilities.Length > 0) { return availableApplicabilities - .OrderByDescending(a => a.Version ?? new SemVersion(0, 0, 0)) - .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .First(); } - var futureApplicabilities = applicabilityList - .Where(a => a.Version is not null && a.Version is not AllVersions && a.Version > currentVersion) - .ToList(); + var futureApplicabilities = applicabilities + .Where(a => a.Version is not null && a.Version is not AllVersionsSpec && a.Version.Min > currentVersion).ToArray(); - if (futureApplicabilities.Count != 0) + if (futureApplicabilities.Length > 0) { return futureApplicabilities - .OrderBy(a => a.Version!.CompareTo(currentVersion)) - .ThenBy(a => lifecycleOrder.GetValueOrDefault(a.Lifecycle, 999)) + .OrderBy(a => a.Version!.Min.CompareTo(currentVersion)) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) .First(); } - return applicabilityList.First(); + return applicabilities.First(); } } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs index 6bfa16b4d..8914890a9 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableTo.cs @@ -64,9 +64,11 @@ public record ApplicableTo Product = AppliesCollection.GenerallyAvailable }; + private static readonly VersionSpec DefaultVersion = VersionSpec.TryParse("9.0", out var v) ? v! : AllVersionsSpec.Instance; + public static ApplicableTo Default { get; } = new() { - Stack = new AppliesCollection([new Applicability { Version = new SemVersion(9, 0, 0), Lifecycle = ProductLifecycle.GenerallyAvailable }]), + Stack = new AppliesCollection([new Applicability { Version = DefaultVersion, Lifecycle = ProductLifecycle.GenerallyAvailable }]), Serverless = ServerlessProjectApplicability.All }; diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs index d3779e525..c8d987064 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToJsonConverter.cs @@ -44,7 +44,7 @@ public class ApplicableToJsonConverter : JsonConverter string? type = null; string? subType = null; var lifecycle = ProductLifecycle.GenerallyAvailable; - SemVersion? version = null; + VersionSpec? version = null; while (reader.Read()) { @@ -72,8 +72,14 @@ public class ApplicableToJsonConverter : JsonConverter break; case "version": var versionStr = reader.GetString(); - if (versionStr != null && SemVersionConverter.TryParse(versionStr, out var v)) - version = v; + if (versionStr != null) + { + // Handle "all" explicitly for AllVersionsSpec + if (string.Equals(versionStr.Trim(), "all", StringComparison.OrdinalIgnoreCase)) + version = AllVersionsSpec.Instance; + else if (VersionSpec.TryParse(versionStr, out var v)) + version = v; + } break; } } diff --git a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs index 1017207e1..b22c59ed4 100644 --- a/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs +++ b/src/Elastic.Documentation/AppliesTo/ApplicableToYamlConverter.cs @@ -256,10 +256,109 @@ private static bool TryGetApplicabilityOverTime(Dictionary dict if (target is null || (target is string s && string.IsNullOrWhiteSpace(s))) availability = AppliesCollection.GenerallyAvailable; else if (target is string stackString) + { availability = AppliesCollection.TryParse(stackString, diagnostics, out var a) ? a : null; + + if (availability is not null) + ValidateApplicabilityCollection(key, availability, diagnostics); + } return availability is not null; } + private static void ValidateApplicabilityCollection(string key, AppliesCollection collection, List<(Severity, string)> diagnostics) + { + var items = collection.ToList(); + + // Rule: Only one version declaration per lifecycle + var lifecycleGroups = items.GroupBy(a => a.Lifecycle).ToList(); + var lifecyclesWithMultipleVersions = lifecycleGroups + .Where(group => group.Count(a => a.Version is not null && a.Version != AllVersionsSpec.Instance) > 1) + .Select(g => g.Key) + .ToList(); + + if (lifecyclesWithMultipleVersions.Count > 0) + { + var lifecycleNames = string.Join(", ", lifecyclesWithMultipleVersions); + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted + $"Key '{key}': Multiple version declarations found for lifecycle(s): {lifecycleNames}. Only one version per lifecycle is allowed.")); + } + + // Rule: Only one item per key can use greater-than syntax + var greaterThanItems = items.Where(a => + a.Version is { Kind: VersionSpecKind.GreaterThanOrEqual } && + a.Version != AllVersionsSpec.Instance).ToList(); + + if (greaterThanItems.Count > 1) + { + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted + $"Key '{key}': Multiple items use greater-than-or-equal syntax. Only one item per key can use this syntax.")); + } + + // Rule: In a range, the first version must be less than or equal the last version + var invalidRanges = items + .Where(a => a.Version is { Kind: VersionSpecKind.Range } && a.Version!.Min.CompareTo(a.Version.Max!) > 0) + .ToList(); + + if (invalidRanges.Count > 0) + { + var rangeDescriptions = invalidRanges.Select(item => + $"{item.Lifecycle} ({item.Version!.Min.Major}.{item.Version.Min.Minor}-{item.Version.Max!.Major}.{item.Version.Max.Minor})"); + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted + $"Key '{key}': Invalid range(s) where first version is greater than last version: {string.Join(", ", rangeDescriptions)}.")); + } + + // Rule: No overlapping version ranges + var versionedItems = items + .Where(a => a.Version is not null && a.Version != AllVersionsSpec.Instance) + .ToList(); + + var hasOverlaps = false; + for (var i = 0; i < versionedItems.Count && !hasOverlaps; i++) + { + for (var j = i + 1; j < versionedItems.Count && !hasOverlaps; j++) + { + if (CheckVersionOverlap(versionedItems[i].Version!, versionedItems[j].Version!)) + hasOverlaps = true; + } + } + + if (hasOverlaps) + { + diagnostics.Add((Severity.Hint, // Temporary downgrade to Hint until the currently available docs are adjusted + $"Key '{key}': Overlapping version ranges detected. Ensure version ranges do not overlap within the same key.")); + } + } + + private static bool CheckVersionOverlap(VersionSpec v1, VersionSpec v2) + { + // Allow overlap in case there is a version bump + if (v1.Kind == VersionSpecKind.Range && v2.Kind == VersionSpecKind.GreaterThanOrEqual && + v1.Max is not null && v1.Max.CompareTo(v2.Min) <= 0) + return false; + if (v2.Kind == VersionSpecKind.Range && v1.Kind == VersionSpecKind.GreaterThanOrEqual && + v2.Max is not null && v2.Max.CompareTo(v1.Min) <= 0) + return false; + + // Get the effective ranges for each version spec + // For GreaterThanOrEqual: [min, infinity) + // For Range: [min, max] + // For Exact: [exact, exact] + + var (v1Min, v1Max) = GetEffectiveRange(v1); + var (v2Min, v2Max) = GetEffectiveRange(v2); + + return v1Min.CompareTo(v2Max ?? AllVersions.Instance) <= 0 && + v2Min.CompareTo(v1Max ?? AllVersions.Instance) <= 0; + } + + private static (SemVersion min, SemVersion? max) GetEffectiveRange(VersionSpec spec) => spec.Kind switch + { + VersionSpecKind.Exact => (spec.Min, spec.Min), + VersionSpecKind.Range => (spec.Min, spec.Max), + VersionSpecKind.GreaterThanOrEqual => (spec.Min, null), + _ => throw new ArgumentOutOfRangeException(nameof(spec), spec.Kind, "Unknown VersionSpecKind") + }; + public void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer) => serializer.Invoke(value, type); } diff --git a/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs b/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs new file mode 100644 index 000000000..811709f43 --- /dev/null +++ b/src/Elastic.Documentation/AppliesTo/ProductLifecycleInfo.cs @@ -0,0 +1,62 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Documentation.AppliesTo; + +/// +/// Provides consolidated metadata for product lifecycle states. +/// +public static class ProductLifecycleInfo +{ + /// + /// Contains all display and ordering information for a lifecycle state. + /// + /// Short name for badges (e.g., "Preview", "Beta", "GA"). + /// Full display text for popovers (e.g., "Generally available", "Preview"). + /// Priority order for sorting (lower = higher priority, GA=0). + public sealed record LifecycleMetadata(string ShortName, string DisplayText, int Order); + + /// + /// Gets the metadata for a given lifecycle state. + /// + public static LifecycleMetadata GetMetadata(ProductLifecycle lifecycle) => + Metadata.GetValueOrDefault(lifecycle, FallbackMetadata); + + /// + /// Gets the short name for a lifecycle (e.g., "Preview", "Beta", "GA"). + /// Used for badge CSS classes and compact display. + /// + public static string GetShortName(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).ShortName; + + /// + /// Gets the full display text for a lifecycle (e.g., "Generally available", "Preview"). + /// Used in popover availability text. + /// + public static string GetDisplayText(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).DisplayText; + + /// + /// Gets the sort order for a lifecycle (lower = higher priority). + /// GA=0, Beta=1, Preview=2, etc. + /// + public static int GetOrder(ProductLifecycle lifecycle) => + GetMetadata(lifecycle).Order; + + private static readonly LifecycleMetadata FallbackMetadata = new("", "", 999); + + private static readonly Dictionary Metadata = new() + { + [ProductLifecycle.GenerallyAvailable] = new("GA", "Generally available", 0), + [ProductLifecycle.Beta] = new("Beta", "Beta", 1), + [ProductLifecycle.TechnicalPreview] = new("Preview", "Preview", 2), + [ProductLifecycle.Planned] = new("Planned", "Planned", 3), + [ProductLifecycle.Deprecated] = new("Deprecated", "Deprecated", 4), + [ProductLifecycle.Removed] = new("Removed", "Removed", 5), + [ProductLifecycle.Unavailable] = new("Unavailable", "Unavailable", 6), + [ProductLifecycle.Development] = new("Development", "Development", 7), + [ProductLifecycle.Discontinued] = new("Discontinued", "Discontinued", 8), + }; +} + diff --git a/src/Elastic.Documentation/SemVersion.cs b/src/Elastic.Documentation/SemVersion.cs index 0516f22e0..6784bb484 100644 --- a/src/Elastic.Documentation/SemVersion.cs +++ b/src/Elastic.Documentation/SemVersion.cs @@ -8,11 +8,16 @@ namespace Elastic.Documentation; -public class AllVersions() : SemVersion(9999, 9999, 9999) +public class AllVersions() : SemVersion(99999, 0, 0) { public static AllVersions Instance { get; } = new(); } +public class ZeroVersion() : SemVersion(0, 0, 0) +{ + public static ZeroVersion Instance { get; } = new(); +} + /// /// A semver2 compatible version. /// diff --git a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs index 7a97730a3..cde2ac16e 100644 --- a/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs +++ b/src/Elastic.Documentation/Serialization/SourceGenerationContext.cs @@ -28,5 +28,6 @@ namespace Elastic.Documentation.Serialization; [JsonSerializable(typeof(Applicability))] [JsonSerializable(typeof(ProductLifecycle))] [JsonSerializable(typeof(SemVersion))] +[JsonSerializable(typeof(VersionSpec))] [JsonSerializable(typeof(string[]))] public sealed partial class SourceGenerationContext : JsonSerializerContext; diff --git a/src/Elastic.Documentation/VersionSpec.cs b/src/Elastic.Documentation/VersionSpec.cs new file mode 100644 index 000000000..85de1cff8 --- /dev/null +++ b/src/Elastic.Documentation/VersionSpec.cs @@ -0,0 +1,254 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics.CodeAnalysis; + +namespace Elastic.Documentation; + +public sealed class AllVersionsSpec : VersionSpec +{ + private AllVersionsSpec() : base(AllVersions.Instance, null, VersionSpecKind.GreaterThanOrEqual) + { + } + + public static AllVersionsSpec Instance { get; } = new(); + + public override string ToString() => "all"; +} + +public enum VersionSpecKind +{ + GreaterThanOrEqual, // x.x, x.x+, x.x.x, x.x.x+ + Range, // x.x-y.y, x.x.x-y.y.y + Exact // =x.x, =x.x.x +} + +/// +/// Represents a version specification that can be a single version with greater-than-or-equal semantics, +/// a range of versions, or an exact version match. +/// +public class VersionSpec : IComparable, IEquatable +{ + /// + /// The minimum version (or the exact version for Exact kind). + /// + public SemVersion Min { get; } + + /// + /// The maximum version for ranges. Null for GreaterThanOrEqual and Exact kinds. + /// + public SemVersion? Max { get; } + + /// + /// The kind of version specification. + /// + public VersionSpecKind Kind { get; } + + // Internal constructor to prevent direct instantiation outside of TryParse + // except for AllVersionsSpec which needs to inherit from this class + protected VersionSpec(SemVersion min, SemVersion? max, VersionSpecKind kind) + { + Min = min; + Max = max; + Kind = kind; + } + + /// + /// Creates an Exact version spec from a SemVersion. + /// + public static VersionSpec Exact(SemVersion version) => new(version, null, VersionSpecKind.Exact); + + /// + /// Creates a Range version spec from two SemVersions. + /// + public static VersionSpec Range(SemVersion min, SemVersion max) => new(min, max, VersionSpecKind.Range); + + /// + /// Creates a GreaterThanOrEqual version spec from a SemVersion. + /// + public static VersionSpec GreaterThanOrEqual(SemVersion min) => new(min, null, VersionSpecKind.GreaterThanOrEqual); + + /// + /// Tries to parse a version specification string. + /// Supports: x.x, x.x+, x.x.x, x.x.x+ (gte), x.x-y.y (range), =x.x (exact) + /// + public static bool TryParse(string? input, [NotNullWhen(true)] out VersionSpec? spec) + { + spec = null; + + if (string.IsNullOrWhiteSpace(input)) + return false; + + var trimmed = input.Trim(); + + // Check for exact syntax: =x.x or =x.x.x + if (trimmed.StartsWith('=')) + { + var versionPart = trimmed[1..]; + if (!TryParseVersion(versionPart, out var version)) + return false; + + spec = new(version, null, VersionSpecKind.Exact); + return true; + } + + // Check for range syntax: x.x-y.y or x.x.x-y.y.y + var dashIndex = FindRangeSeparator(trimmed); + if (dashIndex > 0) + { + var minPart = trimmed[..dashIndex]; + var maxPart = trimmed[(dashIndex + 1)..]; + + if (!TryParseVersion(minPart, out var minVersion) || + !TryParseVersion(maxPart, out var maxVersion)) + return false; + + spec = new(minVersion, maxVersion, VersionSpecKind.Range); + return true; + } + + // Otherwise, it's greater-than-or-equal syntax + // Strip trailing + if present + var versionString = trimmed.EndsWith('+') ? trimmed[..^1] : trimmed; + + if (!TryParseVersion(versionString, out var gteVersion)) + return false; + + spec = new(gteVersion, null, VersionSpecKind.GreaterThanOrEqual); + return true; + } + + /// + /// Finds the position of the dash separator in a range specification. + /// Returns -1 if no valid range separator is found. + /// + private static int FindRangeSeparator(string input) + { + // Look for a dash that's not part of a prerelease version + // We need to distinguish between "9.0-9.1" (range) and "9.0-alpha" (prerelease) + // Strategy: Find dashes and check if what follows looks like a version number + + for (var i = 0; i < input.Length; i++) + { + if (input[i] == '-') + { + // Check if there's content before and after the dash + if (i == 0 || i == input.Length - 1) + continue; + + // Check if the character after dash is a digit (indicating a version) + if (i + 1 < input.Length && char.IsDigit(input[i + 1])) + { + // Also verify that what comes before looks like a version + var beforeDash = input[..i]; + if (TryParseVersion(beforeDash, out _)) + return i; + } + } + } + + return -1; + } + + /// + /// Tries to parse a version string, normalizing minor versions to include patch 0. + /// + private static bool TryParseVersion(string input, [NotNullWhen(true)] out SemVersion? version) + { + version = null; + + if (string.IsNullOrWhiteSpace(input)) + return false; + + var trimmed = input.Trim(); + + // Try to parse as-is first + if (SemVersion.TryParse(trimmed, out version)) + return true; + + // If that fails, try appending .0 to support minor version format (e.g., "9.2" -> "9.2.0") + if (SemVersion.TryParse(trimmed + ".0", out version)) + return true; + + return false; + } + + /// + /// Returns the canonical string representation of this version spec. + /// Format: "9.2+" for GreaterThanOrEqual, "9.0-9.1" for Range, "=9.2" for Exact + /// + public override string ToString() => Kind switch + { + VersionSpecKind.Exact => $"={Min.Major}.{Min.Minor}", + VersionSpecKind.Range => $"{Min.Major}.{Min.Minor}-{Max!.Major}.{Max.Minor}", + VersionSpecKind.GreaterThanOrEqual => $"{Min.Major}.{Min.Minor}+", + _ => throw new ArgumentOutOfRangeException(nameof(Kind), Kind, null) + }; + + /// + /// Compares this VersionSpec to another for sorting. + /// Uses Max for ranges, otherwise uses Min. + /// + public int CompareTo(VersionSpec? other) + { + if (other is null) + return 1; + + // For sorting, we want to compare the "highest" version in each spec + var thisCompareVersion = Kind == VersionSpecKind.Range && Max is not null ? Max : Min; + var otherCompareVersion = other.Kind == VersionSpecKind.Range && other.Max is not null ? other.Max : other.Min; + + return thisCompareVersion.CompareTo(otherCompareVersion); + } + + /// + /// Checks if this VersionSpec is equal to another. + /// + public bool Equals(VersionSpec? other) + { + if (other is null) + return false; + + if (ReferenceEquals(this, other)) + return true; + + return Kind == other.Kind && Min.Equals(other.Min) && + (Max?.Equals(other.Max) ?? (other.Max is null)); + } + + public override bool Equals(object? obj) => obj is VersionSpec other && Equals(other); + + public override int GetHashCode() => HashCode.Combine(Kind, Min, Max); + + public static bool operator ==(VersionSpec? left, VersionSpec? right) + { + if (left is null) + return right is null; + return left.Equals(right); + } + + public static bool operator !=(VersionSpec? left, VersionSpec? right) => !(left == right); + + public static bool operator <(VersionSpec? left, VersionSpec? right) => + left is null ? right is not null : left.CompareTo(right) < 0; + + public static bool operator <=(VersionSpec? left, VersionSpec? right) => + left is null || left.CompareTo(right) <= 0; + + public static bool operator >(VersionSpec? left, VersionSpec? right) => + left is not null && left.CompareTo(right) > 0; + + public static bool operator >=(VersionSpec? left, VersionSpec? right) => + left is null ? right is null : left.CompareTo(right) >= 0; + + /// + /// Explicit conversion from string to VersionSpec + /// + public static explicit operator VersionSpec(string s) + { + if (TryParse(s, out var spec)) + return spec!; + throw new ArgumentException($"'{s}' is not a valid version specification string."); + } +} diff --git a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs index 8ffe34306..57ce08df1 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicabilityRenderer.cs @@ -2,205 +2,448 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; namespace Elastic.Markdown.Myst.Components; -public class ApplicabilityRenderer +public static class ApplicabilityRenderer { + /// + /// Represents a single availability item in the popover (e.g., "Generally available since 9.1"). + /// + public record PopoverAvailabilityItem( + [property: JsonPropertyName("text")] string Text, + [property: JsonPropertyName("lifecycleDescription")] string? LifecycleDescription + ); + + /// + /// Structured data for the popover content, to be serialized as JSON and rendered by the frontend. + /// + public record PopoverData( + [property: JsonPropertyName("productDescription")] string? ProductDescription, + [property: JsonPropertyName("availabilityItems")] PopoverAvailabilityItem[] AvailabilityItems, + [property: JsonPropertyName("additionalInfo")] string? AdditionalInfo, + [property: JsonPropertyName("showVersionNote")] bool ShowVersionNote, + [property: JsonPropertyName("versionNote")] string? VersionNote + ); + public record ApplicabilityRenderData( string BadgeLifecycleText, string Version, - string TooltipText, + PopoverData? PopoverData, string LifecycleClass, + string LifecycleName, bool ShowLifecycleName, bool ShowVersion, bool HasMultipleLifecycles = false ); - public ApplicabilityRenderData RenderApplicability( - Applicability applicability, + public static ApplicabilityRenderData RenderApplicability( + IReadOnlyCollection applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - VersioningSystem versioningSystem, - AppliesCollection allApplications) + VersioningSystem versioningSystem) { - var lifecycleClass = applicability.GetLifeCycleName().ToLowerInvariant().Replace(" ", "-"); - var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle); - var realVersion = TryGetRealVersion(applicability, out var v) ? v : null; + var allApplications = new AppliesCollection([.. applicabilities]); + + // Sort by version (highest first), then by lifecycle priority as tiebreaker + var sortedApplicabilities = applicabilities + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance) + .ThenBy(a => ProductLifecycleInfo.GetOrder(a.Lifecycle)) + .ToList(); - var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull); - var badgeLifecycleText = BuildBadgeLifecycleText(applicability, versioningSystem, realVersion, allApplications); + // Find the first lifecycle that returns displayable badge data (non-empty text or version) + // If all return empty (all unreleased with multiple lifecycles), use the first one and show "Planned" + BadgeData? badgeData = null; + BadgeData? firstBadgeData = null; + Applicability? firstApplicability = null; + + foreach (var applicability in sortedApplicabilities) + { + var candidateBadgeData = GetBadgeData(applicability, versioningSystem, allApplications); + + // Keep track of the first one as fallback + firstBadgeData ??= candidateBadgeData; + firstApplicability ??= applicability; + + // If this candidate has displayable data, use it + if (!string.IsNullOrEmpty(candidateBadgeData.BadgeLifecycleText) || + !string.IsNullOrEmpty(candidateBadgeData.Version)) + { + badgeData = candidateBadgeData; + break; + } + } + + // If we've exhausted all options (none had displayable data), use the first one with "Planned" + // But only for versioned products - unversioned products should show empty badge + if (badgeData is null && firstBadgeData is not null && versioningSystem.IsVersioned()) + badgeData = firstBadgeData with { BadgeLifecycleText = "Planned" }; + + badgeData ??= GetBadgeData(sortedApplicabilities.First(), versioningSystem, allApplications); + + var popoverData = BuildPopoverData(applicabilities, applicabilityDefinition, versioningSystem); + + // Check if there are multiple different lifecycles + var hasMultipleLifecycles = applicabilities.Select(a => a.Lifecycle).Distinct().Count() > 1; - var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); - var showVersion = applicability.Version is not null and not AllVersions && versioningSystem.Current >= applicability.Version; - var version = applicability.Version?.ToString() ?? ""; return new ApplicabilityRenderData( - BadgeLifecycleText: badgeLifecycleText, - Version: version, - TooltipText: tooltipText, - LifecycleClass: lifecycleClass, - ShowLifecycleName: showLifecycle, - ShowVersion: showVersion + BadgeLifecycleText: badgeData.BadgeLifecycleText, + Version: badgeData.Version, + PopoverData: popoverData, + LifecycleClass: badgeData.LifecycleClass, + LifecycleName: badgeData.LifecycleName, + ShowLifecycleName: badgeData.ShowLifecycleName || (string.IsNullOrEmpty(badgeData.BadgeLifecycleText) && hasMultipleLifecycles), + ShowVersion: badgeData.ShowVersion, + HasMultipleLifecycles: hasMultipleLifecycles ); } - public ApplicabilityRenderData RenderCombinedApplicability( - IEnumerable applicabilities, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, + /// + /// Gets the badge display data for a single applicability (used internally for badge rendering decisions). + /// + private static BadgeData GetBadgeData( + Applicability applicability, VersioningSystem versioningSystem, AppliesCollection allApplications) { - var applicabilityList = applicabilities.ToList(); - var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(applicabilityList, versioningSystem.Current); + var lifecycleClass = applicability.GetLifeCycleName().ToLowerInvariant().Replace(" ", "-"); + var badgeLifecycleText = BuildBadgeLifecycleText(applicability, versioningSystem, allApplications); - var primaryRenderData = RenderApplicability(primaryApplicability, applicabilityDefinition, versioningSystem, allApplications); - var combinedTooltip = BuildCombinedTooltipText(applicabilityList, applicabilityDefinition, versioningSystem); + var showLifecycle = applicability.Lifecycle != ProductLifecycle.GenerallyAvailable && string.IsNullOrEmpty(badgeLifecycleText); - // Check if there are multiple different lifecycles - var hasMultipleLifecycles = applicabilityList.Select(a => a.Lifecycle).Distinct().Count() > 1; + // Determine if we should show the version based on VersionSpec + var versionDisplay = GetBadgeVersionText(applicability.Version, versioningSystem); + var showVersion = !string.IsNullOrEmpty(versionDisplay); - return primaryRenderData with + // Special handling for Removed lifecycle - don't show + suffix + if (applicability is { Lifecycle: ProductLifecycle.Removed, Version.Kind: VersionSpecKind.GreaterThanOrEqual } && + !string.IsNullOrEmpty(versionDisplay)) { - TooltipText = combinedTooltip, - HasMultipleLifecycles = hasMultipleLifecycles, - ShowLifecycleName = primaryRenderData.ShowLifecycleName || (string.IsNullOrEmpty(primaryRenderData.BadgeLifecycleText) && hasMultipleLifecycles) - }; + versionDisplay = versionDisplay.TrimEnd('+'); + } + + return new BadgeData( + BadgeLifecycleText: badgeLifecycleText, + Version: versionDisplay, + LifecycleClass: lifecycleClass, + LifecycleName: applicability.GetLifeCycleName(), + ShowLifecycleName: showLifecycle, + ShowVersion: showVersion + ); } + private sealed record BadgeData( + string BadgeLifecycleText, + string Version, + string LifecycleClass, + string LifecycleName, + bool ShowLifecycleName, + bool ShowVersion + ); - private static string BuildCombinedTooltipText( - List applicabilities, + private static PopoverData BuildPopoverData( + IReadOnlyCollection applicabilities, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, VersioningSystem versioningSystem) { - var tooltipParts = new List(); + var productInfo = ProductDescriptions.GetProductInfo(versioningSystem.Id); + var productName = GetPlainProductName(applicabilityDefinition.DisplayName); - // Order by the same logic as primary selection: available first (by version desc), then future (by version asc) + // Availability section - collect items from all applicabilities + // Order by version descending (most recent/future first, then going backwards) var orderedApplicabilities = applicabilities - .OrderByDescending(a => a.Version is null || a.Version is AllVersions || a.Version <= versioningSystem.Current ? 1 : 0) - .ThenByDescending(a => a.Version ?? new SemVersion(0, 0, 0)) - .ThenBy(a => a.Version ?? new SemVersion(0, 0, 0)) - .ToList(); + .OrderByDescending(a => a.Version?.Min ?? ZeroVersion.Instance); + + var showVersionNote = productInfo is { IncludeVersionNote: true } && versioningSystem.IsVersioned(); + + return new PopoverData( + ProductDescription: productInfo?.Description, + AvailabilityItems: orderedApplicabilities.Select(applicability => BuildAvailabilityItem(applicability, versioningSystem, productName, applicabilities.Count)).OfType().ToArray(), + AdditionalInfo: productInfo?.AdditionalAvailabilityInfo, + ShowVersionNote: showVersionNote, + VersionNote: showVersionNote ? ProductDescriptions.VersionNote : null + ); + } + + /// + /// Builds an availability item for an applicability entry. + /// Returns null if the item should not be added to the availability list. + /// + private static PopoverAvailabilityItem? BuildAvailabilityItem( + Applicability applicability, + VersioningSystem versioningSystem, + string productName, + int lifecycleCount) + { + var availabilityText = GenerateAvailabilityText(applicability, versioningSystem, lifecycleCount); + + if (availabilityText is null) + return null; + + var isReleased = IsVersionReleased(applicability, versioningSystem); + var lifecycleDescription = LifecycleDescriptions.GetDescriptionWithProduct( + applicability.Lifecycle, + isReleased, + productName + ); + + return new PopoverAvailabilityItem( + Text: availabilityText, + LifecycleDescription: lifecycleDescription + ); + } + + /// + /// Generates the dynamic availability text based on version type, lifecycle, release status, and lifecycle count. + /// Returns null if the item should not be added to the availability list. + /// + private static string? GenerateAvailabilityText( + Applicability applicability, + VersioningSystem versioningSystem, + int lifecycleCount) + { + var lifecycle = applicability.Lifecycle; + var versionSpec = applicability.Version; - foreach (var applicability in orderedApplicabilities) + if (versionSpec is null or AllVersionsSpec) { - var realVersion = TryGetRealVersion(applicability, out var v) ? v : null; - var lifecycleFull = GetLifecycleFullText(applicability.Lifecycle); - var heading = CreateApplicabilityHeading(applicability, applicabilityDefinition, realVersion); - var tooltipText = BuildTooltipText(applicability, applicabilityDefinition, versioningSystem, realVersion, lifecycleFull); - // language=html - tooltipParts.Add($"
{heading}{tooltipText}
"); + if (!versioningSystem.IsVersioned()) + return ProductLifecycleInfo.GetDisplayText(lifecycle); + + var baseVersion = $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}"; + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {baseVersion}+", + ProductLifecycle.Unavailable => $"Unavailable in {baseVersion}+", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {baseVersion}+" + }; } - return string.Join("\n\n", tooltipParts); + // Get version info + var min = versionSpec.Min; + var max = versionSpec.Max; + var minVersion = $"{min.Major}.{min.Minor}"; + var maxVersion = max is not null ? $"{max.Major}.{max.Minor}" : null; + var isMinReleased = min <= versioningSystem.Current; + var isMaxReleased = max is not null && max <= versioningSystem.Current; + + return versionSpec.Kind switch + { + // Greater than or equal (x.x+, x.x, x.x.x+, x.x.x) + VersionSpecKind.GreaterThanOrEqual => GenerateGteAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), + + // Range (x.x-y.y, x.x.x-y.y.y) + VersionSpecKind.Range => GenerateRangeAvailabilityText(lifecycle, minVersion, maxVersion!, isMinReleased, isMaxReleased, lifecycleCount), + + // Exact (=x.x, =x.x.x) + VersionSpecKind.Exact => GenerateExactAvailabilityText(lifecycle, minVersion, isMinReleased, lifecycleCount), + + _ => null + }; } - private static string CreateApplicabilityHeading(Applicability applicability, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - SemVersion? realVersion) + /// + /// Generates availability text for greater-than-or-equal version type. + /// + private static string? GenerateGteAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) { - var lifecycleName = applicability.GetLifeCycleName(); - var versionText = realVersion is not null ? $" {realVersion}" : ""; - // language=html - return $"""{applicabilityDefinition.DisplayName} {lifecycleName}{versionText}:"""; + if (isReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {version}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {version}" + }; + } + + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable when lifecycleCount == 1 => "Unavailable", + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" + }; } - private static string GetLifecycleFullText(ProductLifecycle lifecycle) => lifecycle switch + /// + /// Generates availability text for range version type. + /// + private static string? GenerateRangeAvailabilityText( + ProductLifecycle lifecycle, string minVersion, string maxVersion, bool isMinReleased, bool isMaxReleased, int lifecycleCount) { - ProductLifecycle.GenerallyAvailable => "Available", - ProductLifecycle.Beta => "Available in beta", - ProductLifecycle.TechnicalPreview => "Available in technical preview", - ProductLifecycle.Deprecated => "Deprecated", - ProductLifecycle.Removed => "Removed", - ProductLifecycle.Unavailable => "Not available", - _ => "" - }; - - private static string BuildTooltipText( - Applicability applicability, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - VersioningSystem versioningSystem, - SemVersion? realVersion, - string lifecycleFull) + if (isMaxReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Unavailable => $"Unavailable from {minVersion} to {maxVersion}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} from {minVersion} to {maxVersion}" + }; + } + + if (isMinReleased) + { + // Max is not released, min is released -> treat as "since min" + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {minVersion}", + ProductLifecycle.Unavailable => $"Unavailable since {minVersion}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} since {minVersion}" + }; + } + + // Neither released + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable => null, + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" + }; + } + + /// + /// Generates availability text for exact version type. + /// + private static string? GenerateExactAvailabilityText(ProductLifecycle lifecycle, string version, bool isReleased, int lifecycleCount) { - var tooltipText = ""; + if (isReleased) + { + return lifecycle switch + { + ProductLifecycle.Removed => $"Removed in {version}", + ProductLifecycle.Unavailable => $"Unavailable in {version}", + _ => $"{ProductLifecycleInfo.GetDisplayText(lifecycle)} in {version}" + }; + } - tooltipText = realVersion is not null - ? realVersion <= versioningSystem.Current - ? $"{lifecycleFull} on {applicabilityDefinition.DisplayName} version {realVersion} and later unless otherwise specified." - : applicability.Lifecycle switch - { - ProductLifecycle.GenerallyAvailable - or ProductLifecycle.Beta - or ProductLifecycle.TechnicalPreview - or ProductLifecycle.Planned => - $"We plan to add this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - ProductLifecycle.Deprecated => - $"We plan to deprecate this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - ProductLifecycle.Removed => - $"We plan to remove this functionality in a future {applicabilityDefinition.DisplayName} update. Subject to change.", - _ => tooltipText - } - : $"{lifecycleFull} on {applicabilityDefinition.DisplayName} unless otherwise specified."; - - var disclaimer = GetDisclaimer(applicability.Lifecycle, versioningSystem.Id); - if (disclaimer is not null) - tooltipText = $"{tooltipText}\n\n{disclaimer}"; - - return tooltipText; + // Unreleased + return lifecycle switch + { + ProductLifecycle.Deprecated => "Planned for deprecation", + ProductLifecycle.Removed => "Planned for removal", + ProductLifecycle.Unavailable => null, + _ when lifecycleCount >= 2 => null, // Do not add to availability list + _ => "Planned" + }; } - private static string? GetDisclaimer(ProductLifecycle lifecycle, VersioningSystemId versioningSystemId) => lifecycle switch + /// + /// Gets the plain product name without HTML entities for use in text substitution. + /// + private static string GetPlainProductName(string displayName) => + displayName.Replace(" ", " "); + + /// + /// Determines if a version should be considered released for lifecycle description purposes + /// For ranges, if min is released, the feature is currently available + /// + private static bool IsVersionReleased(Applicability applicability, VersioningSystem versioningSystem) { - ProductLifecycle.Beta => - "Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.", - ProductLifecycle.TechnicalPreview => - "This functionality may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.", - ProductLifecycle.GenerallyAvailable => versioningSystemId is VersioningSystemId.Stack - ? "If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page." - : null, - _ => null - }; + var versionSpec = applicability.Version; + + // No version specified - consider released + if (versionSpec is null or AllVersionsSpec) + return true; + + // For all version spec types, check if min is released + // This determines whether the feature is currently available + return versionSpec.Min <= versioningSystem.Current; + } private static string BuildBadgeLifecycleText( Applicability applicability, VersioningSystem versioningSystem, - SemVersion? realVersion, AppliesCollection allApplications) { var badgeText = ""; - if (realVersion is not null && realVersion > versioningSystem.Current) + var versionSpec = applicability.Version; + + if (versionSpec is not null && versionSpec != AllVersionsSpec.Instance) { - badgeText = applicability.Lifecycle switch + var isMinReleased = versionSpec.Min <= versioningSystem.Current; + var isMaxReleased = versionSpec.Max is not null && versionSpec.Max <= versioningSystem.Current; + + // Determine if we should show "Planned" badge + var shouldShowPlanned = (versionSpec.Kind == VersionSpecKind.GreaterThanOrEqual && !isMinReleased) + || (versionSpec.Kind == VersionSpecKind.Range && !isMaxReleased && !isMinReleased) + || (versionSpec.Kind == VersionSpecKind.Exact && !isMinReleased); + + // Check lifecycle count for "use previous lifecycle" logic + if (shouldShowPlanned) { - ProductLifecycle.TechnicalPreview => "Planned", - ProductLifecycle.Beta => "Planned", - ProductLifecycle.GenerallyAvailable => - allApplications.Any(a => a.Lifecycle is ProductLifecycle.TechnicalPreview or ProductLifecycle.Beta) - ? "GA planned" - : "Planned", - ProductLifecycle.Deprecated => "Deprecation planned", - ProductLifecycle.Removed => "Removal planned", - ProductLifecycle.Planned => "Planned", - ProductLifecycle.Unavailable => "Unavailable", - _ => badgeText - }; + var lifecycleCount = allApplications.Count; + + // If lifecycle count >= 2, we should use previous lifecycle instead of showing "Planned" + if (lifecycleCount >= 2) + return string.Empty; + + // Otherwise show planned badge (lifecycle count == 1) + badgeText = applicability.Lifecycle switch + { + ProductLifecycle.TechnicalPreview => "Planned", + ProductLifecycle.Beta => "Planned", + ProductLifecycle.GenerallyAvailable => "Planned", + ProductLifecycle.Deprecated => "Deprecation planned", + ProductLifecycle.Removed => "Removal planned", + ProductLifecycle.Planned => "Planned", + ProductLifecycle.Unavailable => "Unavailable", + _ => badgeText + }; + } } return badgeText; } - private static bool TryGetRealVersion(Applicability applicability, [NotNullWhen(true)] out SemVersion? version) + /// + /// Gets the version to display in badges, handling VersionSpec kinds + /// + private static string GetBadgeVersionText(VersionSpec? versionSpec, VersioningSystem versioningSystem) { - version = null; - if (applicability.Version is not null && applicability.Version != AllVersions.Instance) + // When no version is specified (null or AllVersionsSpec), check if we should show the base version + switch (versionSpec) { - version = applicability.Version; - return true; - } + case AllVersionsSpec: + case null: + // Only show base version if the product is versioned + return versioningSystem.IsVersioned() + ? $"{versioningSystem.Base.Major}.{versioningSystem.Base.Minor}+" + : string.Empty; + default: + var kind = versionSpec.Kind; + var min = versionSpec.Min; + var max = versionSpec.Max; + + // Check if versions are released + var minReleased = min <= versioningSystem.Current; + var maxReleased = max is not null && max <= versioningSystem.Current; + + return kind switch + { + VersionSpecKind.GreaterThanOrEqual => minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, - return false; + VersionSpecKind.Range => maxReleased + ? min.Major == max!.Major && min.Minor == max.Minor + ? $"{min.Major}.{min.Minor}" // Same major.minor, so just show the version once + : $"{min.Major}.{min.Minor}-{max.Major}.{max.Minor}" + : minReleased + ? $"{min.Major}.{min.Minor}+" + : string.Empty, + + VersionSpecKind.Exact => minReleased + ? $"{min.Major}.{min.Minor}" + : string.Empty, + + _ => string.Empty + }; + } } } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml index a9831405c..f98c89dc0 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToComponent.cshtml @@ -1,37 +1,20 @@ +@using System.Text.Json +@using Elastic.Markdown.Page @inherits RazorSlice @foreach (var item in Model.GetApplicabilityItems()) { - - @item.Key - - @if (!string.IsNullOrEmpty(item.Key) && (item.RenderData.ShowLifecycleName || item.RenderData.ShowVersion || !string.IsNullOrEmpty(item.RenderData.BadgeLifecycleText))) - { - - } - - @if (item.RenderData.ShowLifecycleName) - { - @item.PrimaryApplicability.GetLifeCycleName() - } - @if (item.RenderData.ShowVersion) - { - - @item.RenderData.Version - - } - else - { - @item.RenderData.BadgeLifecycleText - } - @if (item.RenderData.HasMultipleLifecycles) - { - - - - - - } - - + } diff --git a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs index c2d83697c..ffe651543 100644 --- a/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs +++ b/src/Elastic.Markdown/Myst/Components/ApplicableToViewModel.cs @@ -2,7 +2,6 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using Elastic.Documentation; using Elastic.Documentation.AppliesTo; using Elastic.Documentation.Configuration.Versions; @@ -10,8 +9,6 @@ namespace Elastic.Markdown.Myst.Components; public class ApplicableToViewModel { - private readonly ApplicabilityRenderer _applicabilityRenderer = new(); - public required bool Inline { get; init; } public bool ShowTooltip { get; init; } = true; @@ -61,107 +58,98 @@ public class ApplicableToViewModel }; - public IEnumerable GetApplicabilityItems() + public IReadOnlyCollection GetApplicabilityItems() { - var items = new List(); + var rawItems = new List(); if (AppliesTo.Serverless is not null) { - items.AddRange(AppliesTo.Serverless.AllProjects is not null - ? ProcessSingleCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless) - : ProcessMappedCollections(AppliesTo.Serverless, ServerlessMappings)); + rawItems.AddRange(AppliesTo.Serverless.AllProjects is not null + ? CollectFromCollection(AppliesTo.Serverless.AllProjects, ApplicabilityMappings.Serverless) + : CollectFromMappings(AppliesTo.Serverless, ServerlessMappings)); } if (AppliesTo.Stack is not null) - items.AddRange(ProcessSingleCollection(AppliesTo.Stack, ApplicabilityMappings.Stack)); + rawItems.AddRange(CollectFromCollection(AppliesTo.Stack, ApplicabilityMappings.Stack)); if (AppliesTo.Deployment is not null) - items.AddRange(ProcessMappedCollections(AppliesTo.Deployment, DeploymentMappings)); + rawItems.AddRange(CollectFromMappings(AppliesTo.Deployment, DeploymentMappings)); if (AppliesTo.ProductApplicability is not null) - items.AddRange(ProcessMappedCollections(AppliesTo.ProductApplicability, ProductMappings)); + rawItems.AddRange(CollectFromMappings(AppliesTo.ProductApplicability, ProductMappings)); if (AppliesTo.Product is not null) - items.AddRange(ProcessSingleCollection(AppliesTo.Product, ApplicabilityMappings.Product)); + rawItems.AddRange(CollectFromCollection(AppliesTo.Product, ApplicabilityMappings.Product)); - return CombineItemsByKey(items); + return RenderGroupedItems(rawItems).ToArray(); } - private IEnumerable ProcessSingleCollection(AppliesCollection collection, ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) - { - var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId); - return ProcessApplicabilityCollection(collection, applicabilityDefinition, versioningSystem); - } + /// + /// Collects raw applicability items from a single collection. + /// + private static IEnumerable CollectFromCollection( + AppliesCollection collection, + ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition) => + collection.Select(applicability => new RawApplicabilityItem( + Key: applicabilityDefinition.Key, + Applicability: applicability, + ApplicabilityDefinition: applicabilityDefinition + )); /// - /// Uses mapping dictionary to eliminate repetitive code when processing multiple collections + /// Collects raw applicability items from mapped collections. /// - private IEnumerable ProcessMappedCollections(T source, Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings) + private static IReadOnlyCollection CollectFromMappings( + T source, + Dictionary, ApplicabilityMappings.ApplicabilityDefinition> mappings) { - var items = new List(); + var items = new List(); foreach (var (propertySelector, applicabilityDefinition) in mappings) { var collection = propertySelector(source); if (collection is not null) - items.AddRange(ProcessSingleCollection(collection, applicabilityDefinition)); + items.AddRange(CollectFromCollection(collection, applicabilityDefinition)); } return items; } - private IEnumerable ProcessApplicabilityCollection( - AppliesCollection applications, - ApplicabilityMappings.ApplicabilityDefinition applicabilityDefinition, - VersioningSystem versioningSystem) => - applications.Select(applicability => - { - var renderData = _applicabilityRenderer.RenderApplicability( - applicability, - applicabilityDefinition, - versioningSystem, - applications); - - return new ApplicabilityItem( - Key: applicabilityDefinition.Key, - PrimaryApplicability: applicability, - RenderData: renderData, - ApplicabilityDefinition: applicabilityDefinition - ); - }); - /// - /// Combines multiple applicability items with the same key into a single item with combined tooltip + /// Groups raw items by key and renders each group using the unified renderer. /// - private IEnumerable CombineItemsByKey(List items) => items + private IEnumerable RenderGroupedItems(IReadOnlyCollection rawItems) => + rawItems .GroupBy(item => item.Key) .Select(group => { - if (group.Count() == 1) - return group.First(); - - var firstItem = group.First(); - var allApplicabilities = group.Select(g => g.Applicability).ToList(); - var applicabilityDefinition = firstItem.ApplicabilityDefinition; + var items = group.ToList(); + var applicabilityDefinition = items.First().ApplicabilityDefinition; var versioningSystem = VersionsConfig.GetVersioningSystem(applicabilityDefinition.VersioningSystemId); + var allApplicabilities = items.Select(i => i.Applicability).ToArray(); - var combinedRenderData = _applicabilityRenderer.RenderCombinedApplicability( + var renderData = ApplicabilityRenderer.RenderApplicability( allApplicabilities, applicabilityDefinition, - versioningSystem, - new AppliesCollection(allApplicabilities.ToArray())); + versioningSystem); // Select the closest version to current as the primary display var primaryApplicability = ApplicabilitySelector.GetPrimaryApplicability(allApplicabilities, versioningSystem.Current); return new ApplicabilityItem( - Key: firstItem.Key, + Key: items.First().Key, PrimaryApplicability: primaryApplicability, - RenderData: combinedRenderData, + RenderData: renderData, ApplicabilityDefinition: applicabilityDefinition ); }); - - + /// + /// Intermediate representation before rendering. + /// + private sealed record RawApplicabilityItem( + string Key, + Applicability Applicability, + ApplicabilityMappings.ApplicabilityDefinition ApplicabilityDefinition + ); } diff --git a/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs b/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs new file mode 100644 index 000000000..5a37ced3f --- /dev/null +++ b/src/Elastic.Markdown/Myst/Components/LifecycleDescriptions.cs @@ -0,0 +1,74 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.AppliesTo; + +namespace Elastic.Markdown.Myst.Components; + +/// +/// Contains static lifecycle descriptions for use in applicability popovers. +/// +public static class LifecycleDescriptions +{ + /// + /// Gets the lifecycle description for a given lifecycle and release state. + /// The returned text may contain a {product} placeholder that should be replaced with the product name. + /// + /// The product lifecycle state. + /// Whether the version is released. + /// The description text, or null if not applicable. + private static string? GetDescription(ProductLifecycle lifecycle, bool isReleased) => + Descriptions.GetValueOrDefault((lifecycle, isReleased)); + + /// + /// Gets the lifecycle description with the product name substituted. + /// + /// The product lifecycle state. + /// Whether the version is released. + /// The product name to substitute for {product}. + /// The description text with product name substituted, or null if not applicable. + public static string? GetDescriptionWithProduct(ProductLifecycle lifecycle, bool isReleased, string productName) + { + var description = GetDescription(lifecycle, isReleased); + return description?.Replace("{product}", productName); + } + + private static readonly Dictionary<(ProductLifecycle Lifecycle, bool IsReleased), string> Descriptions = new() + { + // Preview + [(ProductLifecycle.TechnicalPreview, true)] = + "This functionality is in technical preview and is not ready for production usage. Technical preview features may change or be removed at any time. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features. Specific Support terms apply.", + [(ProductLifecycle.TechnicalPreview, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // Beta + [(ProductLifecycle.Beta, true)] = + "This functionality is in beta and is not ready for production usage. For beta features, the design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features. Specific Support terms apply.", + [(ProductLifecycle.Beta, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // GA + [(ProductLifecycle.GenerallyAvailable, true)] = + "This functionality is generally available and ready for production usage.", + [(ProductLifecycle.GenerallyAvailable, false)] = + "We plan to add this functionality in a future {product} update. Subject to changes.", + + // Deprecated + [(ProductLifecycle.Deprecated, true)] = + "This functionality is deprecated. You can still use it, but it'll be removed in a future {product} update.", + [(ProductLifecycle.Deprecated, false)] = + "This functionality is planned to be deprecated in a future {product} update. Subject to changes.", + + // Removed + [(ProductLifecycle.Removed, true)] = + "This functionality was removed. You can no longer use it if you're running on this version or a later one.", + [(ProductLifecycle.Removed, false)] = + "This functionality is planned to be removed in an upcoming {product} update. Subject to changes.", + + // Unavailable + [(ProductLifecycle.Unavailable, true)] = + "{product} doesn't include this functionality." + }; +} + diff --git a/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs new file mode 100644 index 000000000..906c8482f --- /dev/null +++ b/src/Elastic.Markdown/Myst/Components/ProductDescriptions.cs @@ -0,0 +1,192 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.Configuration.Versions; + +namespace Elastic.Markdown.Myst.Components; + +/// +/// Contains static product descriptions for use in applicability popovers. +/// +public static class ProductDescriptions +{ + /// + /// Product information record containing description, additional info, and version note flag. + /// + /// The product description shown at the top of the popover (required). + /// Additional availability information shown near the bottom of the popover (optional). + /// Whether to include the version note at the bottom of the popover. + public record ProductInfo( + string Description, + string? AdditionalAvailabilityInfo, + bool IncludeVersionNote + ); + + /// + /// The version note text shown at the bottom of versioned product popovers. + /// + public const string VersionNote = + "This documentation corresponds to the latest patch available for each minor version. If you're not using the latest patch, check the release notes for changes."; + + public static ProductInfo? GetProductInfo(VersioningSystemId versioningSystemId) => + Descriptions.GetValueOrDefault(versioningSystemId); + + private static readonly Dictionary Descriptions = new() + { + // Stack + [VersioningSystemId.Stack] = new ProductInfo( + Description: "The Elastic Stack includes Elastic's core products such as Elasticsearch, Kibana, Logstash, and Beats.", + AdditionalAvailabilityInfo: "Unless stated otherwise on the page, this functionality is available when your Elastic Stack is deployed on Elastic Cloud Hosted, Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, and self-managed environments.", + IncludeVersionNote: true + ), + + // Serverless + [VersioningSystemId.Serverless] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: "Serverless interfaces and procedures might differ from classic Elastic Stack deployments.", + IncludeVersionNote: false + ), + + // Serverless Project Types + [VersioningSystemId.ElasticsearchProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.ObservabilityProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.SecurityProject] = new ProductInfo( + Description: "Elastic Cloud Serverless projects are autoscaled environments, fully managed by Elastic and available on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + + // Deployment Types + [VersioningSystemId.Ess] = new ProductInfo( + Description: "Elastic Cloud Hosted lets you manage and configure one or more deployments of the versioned Elastic Stack, hosted on Elastic Cloud.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: false + ), + [VersioningSystemId.Ece] = new ProductInfo( + Description: "Elastic Cloud Enterprise is a self-managed orchestration platform for deploying and managing the Elastic Stack at scale.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Eck] = new ProductInfo( + Description: "Elastic Cloud on Kubernetes extends Kubernetes orchestration capabilities to allow you to deploy and manage components of the Elastic Stack.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Self] = new ProductInfo( + Description: "Self-managed deployments are Elastic Stack deployments managed without the assistance of an orchestrator.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // Products + [VersioningSystemId.Ecctl] = new ProductInfo( + Description: "ECCTL is the command line interface for the Elastic Cloud and Elastic Cloud Enterprise APIs.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.Curator] = new ProductInfo( + Description: "Curator is a tool that helps you to manage your Elasticsearch indices and snapshots to save space and improve performance.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // APM Agents + [VersioningSystemId.ApmAgentDotnet] = new ProductInfo( + Description: "The Elastic APM .NET agent enables you to trace the execution of operations in your .NET applications, sending performance metrics and errors to the Elastic APM server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentGo] = new ProductInfo( + Description: "The Elastic APM Go agent enables you to trace the execution of operations in your Go applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentJava] = new ProductInfo( + Description: "The Elastic APM Java agent enables you to trace the execution of operations in your Java applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentNode] = new ProductInfo( + Description: "The Elastic APM Node.js agent enables you to trace the execution of operations in your Node.js applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentPhp] = new ProductInfo( + Description: "The Elastic APM PHP agent enables you to trace the execution of operations in your PHP applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentPython] = new ProductInfo( + Description: "The Elastic APM Python agent enables you to trace the execution of operations in your Python applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentRuby] = new ProductInfo( + Description: "The Elastic APM Ruby agent enables you to trace the execution of operations in your Ruby applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.ApmAgentRumJs] = new ProductInfo( + Description: "The Elastic APM RUM JavaScript agent enables you to trace the execution of operations in your web applications, sending performance metrics and errors to the Elastic APM Server.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + + // EDOT Products + [VersioningSystemId.EdotCollector] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Collector retrieves traces, metrics, and logs from your infrastructure and applications, and forwards them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotIos] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) iOS SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotAndroid] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Android SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotDotnet] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) .NET SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotJava] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Java SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotNode] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Node.js SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotPhp] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) PHP SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotPython] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Python SDK collects performance metrics, traces, and logs in OpenTelemetry format, and sends them to Elastic Observability.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + [VersioningSystemId.EdotCfAws] = new ProductInfo( + Description: "The Elastic Distribution of OpenTelemetry (EDOT) Cloud Forwarder allows you to collect and send your telemetry data to Elastic Observability from AWS, GCP, and Azure.", + AdditionalAvailabilityInfo: null, + IncludeVersionNote: true + ), + }; +} + diff --git a/src/Elastic.Markdown/Page/IndexViewModel.cs b/src/Elastic.Markdown/Page/IndexViewModel.cs index 628972698..4be8a6f52 100644 --- a/src/Elastic.Markdown/Page/IndexViewModel.cs +++ b/src/Elastic.Markdown/Page/IndexViewModel.cs @@ -12,6 +12,7 @@ using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.FileProviders; using Elastic.Markdown.IO; +using Elastic.Markdown.Myst.Components; namespace Elastic.Markdown.Page; @@ -137,4 +138,5 @@ private static Dictionary> GroupByMajorVersion(LegacyPageMa } [JsonSerializable(typeof(VersionDropDownItemViewModel[]))] +[JsonSerializable(typeof(ApplicabilityRenderer.PopoverData))] public partial class ViewModelSerializerContext : JsonSerializerContext; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs index 3b22299f8..1bf241171 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterRoundTripTests.cs @@ -36,8 +36,8 @@ public void RoundTripStackWithVersion() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = new SemVersion(8, 0, 0) }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(7, 17, 0) } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" } ]) }; @@ -57,8 +57,8 @@ public void RoundTripDeploymentAllProperties() Deployment = new DeploymentApplicability { Self = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]), + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]), Ess = AppliesCollection.GenerallyAvailable } }; @@ -82,8 +82,8 @@ public void RoundTripServerlessAllProperties() Serverless = new ServerlessProjectApplicability { Elasticsearch = AppliesCollection.GenerallyAvailable, - Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersionsSpec.Instance }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) } }; @@ -140,9 +140,9 @@ public void RoundTripProductApplicabilityMultipleProducts() ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]), - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]), - EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]) + Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"5.0.0" }]), + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]), + EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]) } }; @@ -165,27 +165,27 @@ public void RoundTripAllProductApplicabilityProperties() ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"5.0.0" }]), - ApmAgentAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]), - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]), - ApmAgentGo = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"2.0.0" }]), - ApmAgentIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.5.0" }]), - ApmAgentJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.30.0" }]), - ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - ApmAgentPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.8.0" }]), - ApmAgentPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"6.0.0" }]), - ApmAgentRuby = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"4.0.0" }]), - ApmAgentRumJs = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"5.0.0" }]), - EdotIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]), - EdotAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.8.0" }]), - EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.9.0" }]), - EdotJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.7.0" }]), - EdotNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.6.0" }]), - EdotPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.5.0" }]), - EdotPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"0.4.0" }]), - EdotCfAws = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.3.0" }]), - EdotCfAzure = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"0.2.0" }]), - EdotCollector = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.0.0" }]) + Curator = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"5.0.0" }]), + ApmAgentAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]), + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]), + ApmAgentGo = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"2.0.0" }]), + ApmAgentIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.5.0" }]), + ApmAgentJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.30.0" }]), + ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + ApmAgentPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.8.0" }]), + ApmAgentPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"6.0.0" }]), + ApmAgentRuby = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"4.0.0" }]), + ApmAgentRumJs = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"5.0.0" }]), + EdotIos = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]), + EdotAndroid = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.8.0" }]), + EdotDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.9.0" }]), + EdotJava = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.7.0" }]), + EdotNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.6.0" }]), + EdotPhp = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.5.0" }]), + EdotPython = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"0.4.0" }]), + EdotCfAws = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.3.0" }]), + EdotCfAzure = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"0.2.0" }]), + EdotCollector = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.0.0" }]) } }; @@ -225,27 +225,27 @@ public void RoundTripComplexAllFieldsPopulated() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" } ]), Deployment = new DeploymentApplicability { Self = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"3.0.0" }]), - Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]), + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"3.0.0" }]), + Eck = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]), Ess = AppliesCollection.GenerallyAvailable }, Serverless = new ServerlessProjectApplicability { Elasticsearch = AppliesCollection.GenerallyAvailable, - Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersions.Instance }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Observability = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = AllVersionsSpec.Instance }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) }, Product = AppliesCollection.GenerallyAvailable, ProductApplicability = new ProductApplicability { Ecctl = AppliesCollection.GenerallyAvailable, - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.2.0" }]) + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.2.0" }]) } }; @@ -274,7 +274,7 @@ public void RoundTripAllLifecycles() { var lifecycles = Enum.GetValues(); var applicabilities = lifecycles.Select(lc => - new Applicability { Lifecycle = lc, Version = (SemVersion)"1.0.0" } + new Applicability { Lifecycle = lc, Version = (VersionSpec)"1.0.0" } ).ToArray(); var original = new ApplicableTo @@ -297,10 +297,10 @@ public void RoundTripMultipleApplicabilitiesInCollection() { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" }, - new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"7.16.0" }, - new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"6.0.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, + new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"7.16.0" }, + new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"6.0.0" } ]) }; @@ -345,16 +345,16 @@ public void RoundTripAllVersionsSerializesAsSemanticVersion() { var original = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersions.Instance }]) + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = AllVersionsSpec.Instance }]) }; var json = JsonSerializer.Serialize(original, _options); - json.Should().Contain("\"version\": \"9999.9999.9999\""); + json.Should().Contain("\"version\": \"all\""); var deserialized = JsonSerializer.Deserialize(json, _options); deserialized.Should().NotBeNull(); deserialized!.Stack.Should().NotBeNull(); - deserialized.Stack!.First().Version.Should().Be(AllVersions.Instance); + deserialized.Stack!.First().Version.Should().Be(AllVersionsSpec.Instance); } [Fact] @@ -365,7 +365,7 @@ public void RoundTripProductAndProductApplicabilityBothPresent() Product = AppliesCollection.GenerallyAvailable, ProductApplicability = new ProductApplicability { - Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"1.0.0" }]) + Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"1.0.0" }]) } }; diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs index fbff52703..4e47b5edd 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ApplicableToJsonConverterSerializationTests.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System.Text.Encodings.Web; using System.Text.Json; using Elastic.Documentation; using Elastic.Documentation.AppliesTo; @@ -13,7 +14,8 @@ public class ApplicableToJsonConverterSerializationTests { private readonly JsonSerializerOptions _options = new() { - WriteIndented = true + WriteIndented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; [Fact] @@ -34,7 +36,7 @@ public void SerializeStackProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -49,7 +51,7 @@ public void SerializeStackWithVersionProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" } ]) }; @@ -64,7 +66,7 @@ public void SerializeStackWithVersionProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "beta", - "version": "8.0.0" + "version": "8.0+" } ] """); @@ -80,12 +82,12 @@ public void SerializeMultipleApplicabilitiesProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"7.17.0" + Version = (VersionSpec)"7.17.0" } ]) }; @@ -100,13 +102,13 @@ public void SerializeMultipleApplicabilitiesProducesCorrectJson() "type": "stack", "sub_type": "stack", "lifecycle": "ga", - "version": "8.0.0" + "version": "8.0+" }, { "type": "stack", "sub_type": "stack", "lifecycle": "beta", - "version": "7.17.0" + "version": "7.17+" } ] """); @@ -123,7 +125,7 @@ public void SerializeDeploymentProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"3.0.0" + Version = (VersionSpec)"3.0.0" } ]), Ess = AppliesCollection.GenerallyAvailable @@ -140,13 +142,13 @@ public void SerializeDeploymentProducesCorrectJson() "type": "deployment", "sub_type": "ece", "lifecycle": "ga", - "version": "3.0.0" + "version": "3.0+" }, { "type": "deployment", "sub_type": "ess", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -163,7 +165,7 @@ public void SerializeServerlessProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" } ]), Security = AppliesCollection.GenerallyAvailable @@ -180,13 +182,13 @@ public void SerializeServerlessProducesCorrectJson() "type": "serverless", "sub_type": "elasticsearch", "lifecycle": "beta", - "version": "1.0.0" + "version": "1.0+" }, { "type": "serverless", "sub_type": "security", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -201,7 +203,7 @@ public void SerializeProductProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, - Version = (SemVersion)"0.5.0" + Version = (VersionSpec)"0.5.0" } ]) }; @@ -216,7 +218,7 @@ public void SerializeProductProducesCorrectJson() "type": "product", "sub_type": "product", "lifecycle": "preview", - "version": "0.5.0" + "version": "0.5+" } ] """); @@ -233,7 +235,7 @@ public void SerializeProductApplicabilityProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.Deprecated, - Version = (SemVersion)"5.0.0" + Version = (VersionSpec)"5.0.0" } ]), ApmAgentDotnet = AppliesCollection.GenerallyAvailable @@ -250,13 +252,13 @@ public void SerializeProductApplicabilityProducesCorrectJson() "type": "product", "sub_type": "ecctl", "lifecycle": "deprecated", - "version": "5.0.0" + "version": "5.0+" }, { "type": "product", "sub_type": "apm-agent-dotnet", "lifecycle": "ga", - "version": "9999.9999.9999" + "version": "all" } ] """); @@ -272,27 +274,27 @@ public void SerializeAllLifecyclesProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Deprecated, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" }, new Applicability { Lifecycle = ProductLifecycle.Removed, - Version = (SemVersion)"1.0.0" + Version = (VersionSpec)"1.0.0" } ]) }; @@ -315,7 +317,7 @@ public void SerializeComplexProducesCorrectJson() new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, - Version = (SemVersion)"8.0.0" + Version = (VersionSpec)"8.0.0" } ]), Deployment = new DeploymentApplicability @@ -364,7 +366,7 @@ public void SerializeValidatesJsonStructure() new Applicability { Lifecycle = ProductLifecycle.Beta, - Version = (SemVersion)"3.0.0" + Version = (VersionSpec)"3.0.0" } ]) } @@ -383,12 +385,12 @@ public void SerializeValidatesJsonStructure() stackEntry.GetProperty("type").GetString().Should().Be("stack"); stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + stackEntry.GetProperty("version").GetString().Should().Be("all"); var deploymentEntry = array[1]; deploymentEntry.GetProperty("type").GetString().Should().Be("deployment"); deploymentEntry.GetProperty("sub_type").GetString().Should().Be("ece"); deploymentEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - deploymentEntry.GetProperty("version").GetString().Should().Be("3.0.0"); + deploymentEntry.GetProperty("version").GetString().Should().Be("3.0+"); } } diff --git a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs index 3cefe5c4a..6e76d6f84 100644 --- a/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs +++ b/tests/Elastic.Markdown.Tests/AppliesTo/ProductApplicabilityToStringTests.cs @@ -50,7 +50,7 @@ public void ProductApplicabilityToStringWithSomePropertiesOnlyIncludesSetPropert var productApplicability = new ProductApplicability { ApmAgentDotnet = AppliesCollection.GenerallyAvailable, - Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = new SemVersion(1, 0, 0) }]) + Ecctl = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = VersionSpec.TryParse("1.0.0", out var v) ? v : null }]) }; var result = productApplicability.ToString(); diff --git a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs index 49d427b2b..5af9d58af 100644 --- a/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/AdmonitionTests.cs @@ -267,7 +267,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -299,7 +299,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -331,7 +331,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -363,7 +363,7 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } @@ -395,6 +395,6 @@ public void RendersAppliesToInHtml() var html = Html; html.Should().Contain("applies applies-admonition"); html.Should().Contain("admonition-title__separator"); - html.Should().Contain("applicable-info"); + html.Should().Contain("applies-to-popover"); } } diff --git a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs index f0f372d2d..b60ea8d17 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ApplicabilitySwitchTests.cs @@ -227,11 +227,12 @@ public void GeneratesDeterministicSyncKeysAcrossMultipleRuns() var expectedKeys = new Dictionary { // These are the actual SHA256-based hashes that should never change - { "stack: ga 9.1", "applies-031B7112" }, - { "stack: preview 9.0", "applies-361F73DC" }, - { "ess: ga 8.11", "applies-32E204F7" }, - { "deployment: { ece: ga 9.0, ess: ga 9.1 }", "applies-D099CDEF" }, - { "serverless: all", "applies-A34B17C6" }, + // (unless the version format actually changes) + { "stack: ga 9.1", "applies-A8B9CC9C" }, + { "stack: preview 9.0", "applies-66AECC4E" }, + { "ess: ga 8.11", "applies-9CA8543E" }, + { "deployment: { ece: ga 9.0, ess: ga 9.1 }", "applies-51C670D4" }, + { "serverless: all", "applies-A34B17C6" } }; foreach (var (definition, expectedKey) in expectedKeys) diff --git a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs index e2090cd0c..def825530 100644 --- a/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs +++ b/tests/Elastic.Markdown.Tests/Search/DocumentationDocumentSerializationTests.cs @@ -47,7 +47,7 @@ public void SerializeDocumentWithStackAppliesToProducesCorrectJson() stackEntry.GetProperty("type").GetString().Should().Be("stack"); stackEntry.GetProperty("sub_type").GetString().Should().Be("stack"); stackEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - stackEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + stackEntry.GetProperty("version").GetString().Should().Be("all"); } [Fact] @@ -64,7 +64,7 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() Deployment = new DeploymentApplicability { Ess = AppliesCollection.GenerallyAvailable, - Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"3.5.0" }]) + Ece = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"3.5.0" }]) } } }; @@ -82,14 +82,14 @@ public void SerializeDocumentWithDeploymentAppliesToProducesCorrectJson() essEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); essEntry.GetProperty("type").GetString().Should().Be("deployment"); essEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - essEntry.GetProperty("version").GetString().Should().Be("9999.9999.9999"); + essEntry.GetProperty("version").GetString().Should().Be("all"); // Verify ECE entry var eceEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "ece"); eceEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); eceEntry.GetProperty("type").GetString().Should().Be("deployment"); eceEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - eceEntry.GetProperty("version").GetString().Should().Be("3.5.0"); + eceEntry.GetProperty("version").GetString().Should().Be("3.5+"); } [Fact] @@ -105,8 +105,8 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() { Serverless = new ServerlessProjectApplicability { - Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]), - Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (SemVersion)"1.0.0" }]) + Elasticsearch = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), + Security = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.TechnicalPreview, Version = (VersionSpec)"1.0.0" }]) } } }; @@ -124,14 +124,14 @@ public void SerializeDocumentWithServerlessAppliesToProducesCorrectJson() esEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); esEntry.GetProperty("type").GetString().Should().Be("serverless"); esEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - esEntry.GetProperty("version").GetString().Should().Be("8.0.0"); + esEntry.GetProperty("version").GetString().Should().Be("8.0+"); // Verify security entry var secEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "security"); secEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); secEntry.GetProperty("type").GetString().Should().Be("serverless"); secEntry.GetProperty("lifecycle").GetString().Should().Be("preview"); - secEntry.GetProperty("version").GetString().Should().Be("1.0.0"); + secEntry.GetProperty("version").GetString().Should().Be("1.0+"); } [Fact] @@ -145,7 +145,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() SearchTitle = "Product Test", Applies = new ApplicableTo { - Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"2.0.0" }]) + Product = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"2.0.0" }]) } }; @@ -161,7 +161,7 @@ public void SerializeDocumentWithProductAppliesToProducesCorrectJson() productEntry.GetProperty("type").GetString().Should().Be("product"); productEntry.GetProperty("sub_type").GetString().Should().Be("product"); productEntry.GetProperty("lifecycle").GetString().Should().Be("beta"); - productEntry.GetProperty("version").GetString().Should().Be("2.0.0"); + productEntry.GetProperty("version").GetString().Should().Be("2.0+"); } [Fact] @@ -177,8 +177,8 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() { ProductApplicability = new ProductApplicability { - ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"1.5.0" }]), - ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"2.0.0" }]) + ApmAgentDotnet = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"1.5.0" }]), + ApmAgentNode = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"2.0.0" }]) } } }; @@ -196,14 +196,14 @@ public void SerializeDocumentWithProductApplicabilityProducesCorrectJson() dotnetEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); dotnetEntry.GetProperty("type").GetString().Should().Be("product"); dotnetEntry.GetProperty("lifecycle").GetString().Should().Be("ga"); - dotnetEntry.GetProperty("version").GetString().Should().Be("1.5.0"); + dotnetEntry.GetProperty("version").GetString().Should().Be("1.5+"); // Verify apm-agent-node entry var nodeEntry = appliesArray.FirstOrDefault(e => e.GetProperty("sub_type").GetString() == "apm-agent-node"); nodeEntry.ValueKind.Should().NotBe(JsonValueKind.Undefined); nodeEntry.GetProperty("type").GetString().Should().Be("product"); nodeEntry.GetProperty("lifecycle").GetString().Should().Be("deprecated"); - nodeEntry.GetProperty("version").GetString().Should().Be("2.0.0"); + nodeEntry.GetProperty("version").GetString().Should().Be("2.0+"); } [Fact] @@ -217,7 +217,7 @@ public void SerializeDocumentWithComplexAppliesToProducesCorrectJson() SearchTitle = "Complex Test", Applies = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }]), + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }]), Deployment = new DeploymentApplicability { Ess = AppliesCollection.GenerallyAvailable @@ -305,10 +305,10 @@ public void RoundTripDocumentWithAppliesToPreservesData() LastUpdated = DateTimeOffset.Parse("2024-01-15T09:00:00Z", CultureInfo.InvariantCulture), Applies = new ApplicableTo { - Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.5.0" }]), + Stack = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.5.0" }]), Deployment = new DeploymentApplicability { - Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"8.6.0" }]) + Ess = new AppliesCollection([new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"8.6.0" }]) } }, Headings = ["Introduction", "Getting Started"], @@ -343,9 +343,9 @@ public void SerializeDocumentWithMultipleApplicabilitiesPerTypeProducesMultipleA { Stack = new AppliesCollection( [ - new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (SemVersion)"8.0.0" }, - new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (SemVersion)"7.17.0" }, - new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (SemVersion)"7.0.0" } + new Applicability { Lifecycle = ProductLifecycle.GenerallyAvailable, Version = (VersionSpec)"8.0.0" }, + new Applicability { Lifecycle = ProductLifecycle.Beta, Version = (VersionSpec)"7.17.0" }, + new Applicability { Lifecycle = ProductLifecycle.Deprecated, Version = (VersionSpec)"7.0.0" } ]) } }; diff --git a/tests/authoring/Applicability/ApplicableToComponent.fs b/tests/authoring/Applicability/ApplicableToComponent.fs index 2878c4a95..eddfdda62 100644 --- a/tests/authoring/Applicability/ApplicableToComponent.fs +++ b/tests/authoring/Applicability/ApplicableToComponent.fs @@ -12,7 +12,7 @@ open Swensen.Unquote open Xunit // Test Stack applicability scenarios -type ``stack applicability tests`` () = +type ``stack ga future applicability`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 9.0.0 @@ -31,15 +31,8 @@ stack: ga 9.0.0 let ``renders GA with version`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" @@ -54,19 +47,12 @@ stack: preview 9.1.0 let ``renders preview future version as planned`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" -type ``stack beta current version`` () = +type ``stack beta future version`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: beta 8.8.0 @@ -74,22 +60,15 @@ stack: beta 8.8.0 """ [] - let ``renders beta current version`` () = + let ``renders beta future version`` () = markdown |> convertsToHtml """

- - Stack - - - Planned - - + +

""" -type ``stack deprecated`` () = +type ``stack planned deprecation`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: deprecated 8.7.0 @@ -97,20 +76,15 @@ stack: deprecated 8.7.0 """ [] - let ``renders deprecated`` () = + let ``renders deprecation planned`` () = markdown |> convertsToHtml """

- - Stack - - - Deprecation planned - - + +

""" -type ``stack removed`` () = +type ``stack removal planned`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: removed 8.6.0 @@ -118,20 +92,15 @@ stack: removed 8.6.0 """ [] - let ``renders removed`` () = + let ``renders planned for removal`` () = markdown |> convertsToHtml """

- - Stack - - - Removal planned - - + +

""" -type ``stack all versions`` () = +type ``stack ga base version`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga @@ -139,21 +108,16 @@ stack: ga """ [] - let ``renders all versions`` () = + let ``renders ga base version`` () = markdown |> convertsToHtml """

- - Stack - - - + +

""" // Test Serverless applicability scenarios -type ``serverless all projects`` () = +type ``serverless ga future`` () = static let markdown = Setup.Markdown """ ```{applies_to} serverless: ga 9.0.0 @@ -161,16 +125,11 @@ serverless: ga 9.0.0 """ [] - let ``renders serverless all projects`` () = + let ``renders serverless ga planned`` () = markdown |> convertsToHtml """

- - Serverless - - - Planned - - + +

""" @@ -188,31 +147,12 @@ serverless: let ``renders serverless individual projects`` () = markdown |> convertsToHtml """

- - Serverless Elasticsearch - - - Planned - - - - Serverless Observability - - - Planned - - - - Serverless Security - - - Planned - - + + + + + +

""" @@ -229,13 +169,11 @@ deployment: let ``renders ECE deployment`` () = markdown |> convertsToHtml """

- - ECE - - - Planned - - + +

""" @@ -251,15 +189,8 @@ deployment: let ``renders ECK deployment`` () = markdown |> convertsToHtml """

- - ECK - - - Planned - - + +

""" @@ -275,15 +206,8 @@ deployment: let ``renders ECH deployment`` () = markdown |> convertsToHtml """

- - ECH - - - Planned - - + +

""" @@ -299,18 +223,13 @@ deployment: let ``renders self-managed deployment`` () = markdown |> convertsToHtml """

- - Self-Managed - - - Planned - - -

+ + +

""" // Test Product applicability scenarios -type ``apm agents`` () = +type ``apm agents future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} apm_agent_dotnet: ga 9.0.0 @@ -320,38 +239,28 @@ apm_agent_python: preview 9.2.0 """ [] - let ``renders APM agents`` () = + let ``renders APM agents planned`` () = markdown |> convertsToHtml """

- - APM Agent .NET - - - Planned - - - - APM Agent Java - - - Planned - - - - APM Agent Python - - - Planned - - + + + + + +

""" -type ``edot agents`` () = +type ``edot agents future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} edot_dotnet: ga 9.0.0 @@ -361,39 +270,20 @@ edot_python: preview 9.2.0 """ [] - let ``renders EDOT agents`` () = + let ``renders EDOT agents planned`` () = markdown |> convertsToHtml """

- - EDOT .NET - - - Planned - - - - EDOT Java - - - Planned - - - - EDOT Python - - - Planned - - + + + + + +

""" // Test complex scenarios with multiple lifecycles -type ``mixed lifecycles with ga planned`` () = +type ``mixed unreleased lifecycles falls back to planned`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: ga 8.8.0, preview 8.1.0 @@ -401,27 +291,11 @@ stack: ga 8.8.0, preview 8.1.0 """ [] - let ``renders GA planned when preview exists alongside GA`` () = + let ``renders Planned when GA and Preview are both unreleased`` () = markdown |> convertsToHtml """

- - Stack - - - GA planned - - - - - - - + +

""" @@ -436,13 +310,8 @@ stack: deprecated 9.1.0 let ``renders deprecation planned for future version`` () = markdown |> convertsToHtml """

- - Stack - - - Deprecation planned - - + +

""" @@ -457,13 +326,8 @@ stack: removed 9.1.0 let ``renders removal planned for future version`` () = markdown |> convertsToHtml """

- - Stack - - - Removal planned - - + +

""" @@ -479,14 +343,9 @@ stack: unavailable let ``renders unavailable`` () = markdown |> convertsToHtml """

- - Stack - - - Unavailable - - - + + +

""" type ``product all versions`` () = @@ -500,11 +359,8 @@ product: ga let ``renders product all versions`` () = markdown |> convertsToHtml """

- - - - - + +

""" @@ -519,17 +375,8 @@ product: preview 1.3.0 let ``renders product preview`` () = markdown |> convertsToHtml """

- - - - Preview - - 1.3.0 - - - + +

""" @@ -554,70 +401,24 @@ apm_agent_java: beta 9.1.0 let ``renders complex mixed scenario`` () = markdown |> convertsToHtml """

- - Serverless Elasticsearch - - - Planned - - - - Serverless Observability - - - Planned - - - - Stack - - - Planned - - - - ECK - - - Planned - - - - ECE - - - Planned - - - - APM Agent .NET - - - Planned - - - - APM Agent Java - - - Planned - - -

-""" - -// Test missing lifecycle scenarios -type ``lifecycle scenarios missing`` () = + + + + + + + + + + + + + + +

+""" + +type ``stack and ece future versions`` () = static let markdown = Setup.Markdown """ ```{applies_to} stack: beta 9.1.0 @@ -627,211 +428,228 @@ deployment: """ [] - let ``renders missing lifecycle scenarios`` () = + let ``renders stack and ece planned`` () = markdown |> convertsToHtml """

- + + + +

+""" -Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features."> - Stack - - - Planned - - - - ECE - - - Planned - - +type ``stack empty defaults to ga`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: +``` +""" + + [] + let ``no version defaults to ga`` () = + markdown |> convertsToHtml """ +

+ +

""" -// Test missing version scenarios -type ``version scenarios missing`` () = +// Test missing VersioningSystemId coverage +type ``all products future version coverage`` () = static let markdown = Setup.Markdown """ ```{applies_to} -stack: beta 9.1.0 +stack: ga 9.0.0 +serverless: ga 9.0.0 deployment: - ece: ga 9.1.0 + ece: ga 9.0.0 + eck: ga 9.0.0 + ess: ga 9.0.0 + self: ga 9.0.0 +product: ga 9.0.0 ``` """ [] - let ``renders missing version scenarios`` () = + let ``renders VersioningSystemId coverage`` () = markdown |> convertsToHtml """

- + + + + + + + + + + + + + +

+""" -Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features."> - Stack - - - Planned - - - - ECE - - - Planned - - +// Test multiple lifecycles for same applicability key +// With version inference: ga 8.0, beta 8.1 → ga =8.0 (exact), beta 8.1+ (highest gets GTE) +type ``ga with beta uses version inference`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga 8.0.0, beta 8.1.0 +``` +""" + + [] + let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = + markdown |> convertsToHtml """ +

+ +

""" -// Test missing edge cases -type ``edge cases missing`` () = +type ``stack ga released version`` () = static let markdown = Setup.Markdown """ ```{applies_to} -stack: +stack: ga 7.0.0 +``` +""" + + [] + let ``renders ga since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack preview released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: preview 7.0.0 ``` """ [] - let ``renders missing edge cases`` () = + let ``renders preview since released version`` () = markdown |> convertsToHtml """

- + +

+""" + +type ``stack beta released version`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: beta 7.0.0 +``` +""" -If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page."> - Stack - - - + [] + let ``renders beta since released version`` () = + markdown |> convertsToHtml """ +

+ +

""" -// Test missing VersioningSystemId coverage -type ``versioning system id coverage`` () = +type ``stack deprecated released version`` () = static let markdown = Setup.Markdown """ ```{applies_to} -stack: ga 9.0.0 -serverless: ga 9.0.0 -deployment: - ece: ga 9.0.0 - eck: ga 9.0.0 - ess: ga 9.0.0 - self: ga 9.0.0 -product: ga 9.0.0 +stack: deprecated 7.0.0 ``` """ [] - let ``renders missing VersioningSystemId coverage`` () = - markdown |> convertsToHtml """ -

- - Serverless - - - Planned - - - - Stack - - - Planned - - - - ECH - - - Planned - - - - ECK - - - Planned - - - - ECE - - - Planned - - - - Self-Managed - - - Planned - - - - - - Planned - - -

-""" - -// Test missing disclaimer scenarios -type ``disclaimer scenarios`` () = + let ``renders deprecated since released version`` () = + markdown |> convertsToHtml """ +

+ + +

+""" + +type ``stack removed released version`` () = static let markdown = Setup.Markdown """ ```{applies_to} -stack: ga 9.0.0 +stack: removed 7.0.0 ``` """ [] - let ``renders missing disclaimer scenarios`` () = + let ``renders removed in released version`` () = markdown |> convertsToHtml """

- + +

+""" + +// Version spec syntax tests (exact and range) +type ``stack ga exact version released`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga =7.5 +``` +""" -If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page."> - Stack - - - Planned - - + [] + let ``renders ga in exact released version`` () = + markdown |> convertsToHtml """ +

+ +

""" -// Test multiple lifecycles for same applicability key -type ``multiple lifecycles same key`` () = +type ``stack ga range both released`` () = static let markdown = Setup.Markdown """ ```{applies_to} -stack: ga 8.0.0, beta 8.1.0 +stack: ga 7.0-8.0 ``` """ [] - let ``renders multiple lifecycles with ellipsis and shows GA lifecycle`` () = + let ``renders ga from-to when both ends released`` () = markdown |> convertsToHtml """

- + +

+""" -If this functionality is unavailable or behaves differently when deployed on ECH, ECE, ECK, or a self-managed installation, it will be indicated on the page. +type ``stack ga range max unreleased`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: ga 7.0-9.0 +``` +""" -
Elastic Stack Beta 8.1.0:We plan to add this functionality in a future Elastic Stack update. Subject to change. + [] + let ``renders ga since min when max unreleased`` () = + markdown |> convertsToHtml """ +

+ + +

+""" -Beta features are subject to change. The design and code is less mature than official GA features and is being provided as-is with no warranties. Beta features are not subject to the support SLA of official GA features.
"> - Stack - - - GA - - 8.0.0 - - - - - - - - +// Multiple released lifecycles showing both in popover +type ``preview and ga both released`` () = + static let markdown = Setup.Markdown """ +```{applies_to} +stack: preview 7.0, ga 7.5 +``` +""" + + [] + let ``renders ga badge with both lifecycles in popover`` () = + markdown |> convertsToHtml """ +

+ +

""" diff --git a/tests/authoring/Applicability/AppliesToFrontMatter.fs b/tests/authoring/Applicability/AppliesToFrontMatter.fs index 2d1f02b95..95483372e 100644 --- a/tests/authoring/Applicability/AppliesToFrontMatter.fs +++ b/tests/authoring/Applicability/AppliesToFrontMatter.fs @@ -163,10 +163,7 @@ applies_to: [] let ``apply matches expected`` () = markdown |> appliesTo (ApplicableTo( - Product=AppliesCollection([ - Applicability.op_Explicit "removed 9.7"; - Applicability.op_Explicit "preview 9.5" - ] |> Array.ofList) + Product=AppliesCollection.op_Explicit "removed 9.7, preview 9.5" )) type ``lenient to defining types at top level`` () = diff --git a/tests/authoring/Blocks/Admonitions.fs b/tests/authoring/Blocks/Admonitions.fs index d7efdb64b..63525a2c2 100644 --- a/tests/authoring/Blocks/Admonitions.fs +++ b/tests/authoring/Blocks/Admonitions.fs @@ -64,13 +64,7 @@ This is a custom admonition with applies_to information.
Note - - Stack - - - +
@@ -82,11 +76,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Warning - - Serverless - - - +
@@ -98,15 +88,7 @@ If this functionality is unavailable or behaves differently when deployed on ECH
Tip - - Serverless Elasticsearch - - - Preview - - +
diff --git a/tests/authoring/Inline/AppliesToRole.fs b/tests/authoring/Inline/AppliesToRole.fs index 6583a0d79..5600c6a15 100644 --- a/tests/authoring/Inline/AppliesToRole.fs +++ b/tests/authoring/Inline/AppliesToRole.fs @@ -30,15 +30,8 @@ This is an inline {applies_to}`stack: preview 9.1` element. markdown |> convertsToHtml """

This is an inline - - Stack - - - Planned - - + + element.

""" @@ -122,10 +115,10 @@ type ``parses multiple applies_to in one line`` () = ) )) -type ``render 'GA Planned' if preview exists alongside ga`` () = +type ``render 'Preview' for GA in future version`` () = static let markdown = Setup.Markdown """ -This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. +This is an inline {applies_to}`stack: preview 8.0, ga 8.1` element. """ [] @@ -133,7 +126,7 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. let directives = markdown |> converts "index.md" |> parses test <@ directives.Length = 1 @> directives |> appliesToDirective (ApplicableTo( - Stack=AppliesCollection.op_Explicit "ga 9.1, preview 9.0" + Stack=AppliesCollection.op_Explicit "ga 8.1, preview 8.0" )) [] @@ -141,24 +134,8 @@ This is an inline {applies_to}`stack: preview 9.0, ga 9.1` element. markdown |> convertsToHtml """

This is an inline - - Stack - - - GA planned - - - - - - - + + element.

"""