Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
184 changes: 167 additions & 17 deletions .github/scripts/generate_resource_references.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,154 @@

import os
import sys
import json
import re

# Generated resource pages (namespace indexes, API-version indexes, and
# individual resource schema pages) omit a description. The listing partials
# render the page title as the link text, so a "Reference documentation for
# <title>" description would only repeat the title.
hugo_template = """---
type: docs
title: "Reference: {}"
title: "{}"
linkTitle: "{}"
description: "Detailed reference documentation for {}"
---

"""

# Namespace segment display-name overrides, used for capitalization that does
# not follow simple title-casing (e.g. "ai" -> "AI").
namespace_segment_overrides = {"ai": "AI"}

# Namespace-parent directories to skip. The legacy "applications" tree holds the
# retired Applications.* resource types, which are no longer documented.
excluded_namespace_parents = {"applications"}

def display_name(namespace):
return ".".join(
namespace_segment_overrides.get(segment, segment.capitalize())
for segment in namespace.split(".")
)

# The resource markdown filenames are all lowercase (e.g. "bicepsettings.md"),
# but the canonical resource name is camelCased (e.g. "bicepSettings"). The
# authoritative casing lives in the sibling "types.json" as fully qualified type
# names like "Radius.Core/bicepSettings@2025-08-01-preview". Build a map from the
# lowercased resource segment to its canonical camelCase spelling so headings and
# link titles use the correct casing.
def load_canonical_resource_names(types_json_path):
canonical = {}
if not os.path.exists(types_json_path):
return canonical
try:
data = json.load(open(types_json_path, 'r'))
except (ValueError, OSError):
return canonical

def walk(node):
if isinstance(node, dict):
for key, value in node.items():
if key == 'name' and isinstance(value, str) and '/' in value and '@' in value:
resource_segment = value.split('/', 1)[1].split('@', 1)[0]
canonical[resource_segment.lower()] = resource_segment
walk(value)
elif isinstance(node, list):
for item in node:
walk(item)

walk(data)
return canonical

# The emitter wraps the resource @doc prose under a top-level "## Description"
# heading, but the authored prose uses "##" for its own subsections (e.g.
# "Defining an Application"), which makes them siblings of Description instead of
# children. Demote every "##" heading between "## Description" and the first
# schema section to "###", leaving "## Description" and the schema section
# headings untouched. Fenced code blocks are skipped so "##" inside code is not
# altered.
def demote_description_headings(markdown_content):
schema_headings = ("## Top-Level Properties", "## Object Properties", "## Properties")
in_description = False
in_fence = False
result = []
for line in markdown_content.splitlines(keepends=True):
stripped = line.rstrip("\n")
if stripped.startswith("```"):
in_fence = not in_fence
result.append(line)
continue
if not in_fence:
if stripped == "## Description":
in_description = True
result.append(line)
continue
if in_description and stripped in schema_headings:
in_description = False
if in_description and stripped.startswith("## "):
line = "#" + line
result.append(line)
return "".join(result)

# Each schema table carries "Required" and "Read-Only" columns that only ever
# hold "false"/"true". That information is already restated in the Description
# column, which begins with "(Optional)", "(Required)", or "(Read Only)", so the
# dedicated boolean columns are redundant and waste horizontal space (crowding
# the Description). Drop those columns from every table, keeping Property, Type,
# and Description. Columns to remove are identified per-table from the header row.
# Cells are split on unescaped pipes only, so enum unions such as
# "'A' \| 'B' \| 'C'" in the Type column are kept intact rather than mistaken for
# extra columns.
_UNESCAPED_PIPE = re.compile(r"(?<!\\)\|")

def strip_boolean_columns(markdown_content):
columns_to_drop = {"Required", "Read-Only"}
drop_indices = None
result = []
for line in markdown_content.splitlines(keepends=True):
newline = "\n" if line.endswith("\n") else ""
stripped = line.strip()
if stripped.startswith("|") and stripped.endswith("|"):
# Split on unescaped pipes; drop the empty leading/trailing cells
# produced by the outer table pipes.
raw_cells = _UNESCAPED_PIPE.split(stripped)[1:-1]
cells = [c.strip() for c in raw_cells]
if drop_indices is None and columns_to_drop.issubset(set(cells)):
drop_indices = {idx for idx, c in enumerate(cells) if c in columns_to_drop}
if drop_indices is not None:
kept = [c for idx, c in enumerate(raw_cells) if idx not in drop_indices]
result.append("|" + "|".join(kept) + "|" + newline)
continue
else:
# A non-table line ends the current table.
drop_indices = None
result.append(line)
return "".join(result)

# Top-level section index for the flattened resource schema tree. The
# namespace-parent directory level (e.g. "radius") is flattened away so the
# namespaces appear directly under a single "Resource schemas" section.
section_index_template = """---
type: docs
title: "Resource Types"
linkTitle: "Resource Types"
description: "Schema reference for built-in Resource Types"
weight: 200
---

## Introduction

Resource Types define the schema for the resources developers use to model their applications—the properties you can configure, the values Radius returns, and the API versions each type supports. For a deeper explanation of what Resource Types are and how they abstract the underlying infrastructure, see the [Resource Types concepts]({{< ref "concepts/resource-types" >}}) page.

`Radius.Core` Resource Types are embedded in the Radius control plane. All other Resource Types are sourced from the [resource-types-contrib](https://github.com/radius-project/resource-types-contrib) repository.

All of the schema information on these pages is also available directly from your environment using [`rad resource-type list`]({{< ref rad_resource-type_list >}}) and [`rad resource-type show`]({{< ref rad_resource-type_show >}}), as well as through the [Radius Dashboard]({{< ref "/guides/installation/dashboard/overview" >}}).

## How this section is organized

Resource Types are organized first by namespace (such as `Radius.Core`, `Radius.Compute`, and `Radius.Data`) and then by API version (for example, `2025-08-01-preview`). Open a Resource Type to view its schema reference, which documents the resource's properties, including which fields are required and read-only.

"""

# Ensure that the script is called with the correct number of arguments
if len(sys.argv) != 3:
print("Usage: python generate_resource_references.py <source_directory> <target_directory>")
Expand All @@ -53,16 +191,19 @@
print("No namespace parents found in source directory: {}".format(source_directory))
sys.exit(1)

# Create the top-level "Resource schemas" section index.
os.makedirs(target_directory, exist_ok=True)
with open(os.path.join(target_directory, '_index.md'), 'w') as f:
f.write(section_index_template)

# Iterate through each namespace parent directory for each namespace
for namespace_parent in namespace_parents:
if not os.path.isdir(os.path.join(source_directory, namespace_parent)):
continue

# Create _index.md file for namespace parent
target_namespace_parent_dir = os.path.join(target_directory, namespace_parent, '_index.md')
os.makedirs(os.path.dirname(target_namespace_parent_dir), exist_ok=True)
with open(target_namespace_parent_dir, 'w') as f:
f.write(hugo_template.format(namespace_parent, namespace_parent, namespace_parent))
# Skip retired namespace-parent trees (e.g. legacy Applications.* types).
if namespace_parent in excluded_namespace_parents:
continue

namespaces = os.listdir(os.path.join(source_directory, namespace_parent))
if not namespaces:
Expand All @@ -73,11 +214,11 @@
if not os.path.isdir(os.path.join(source_directory, namespace_parent, namespace)):
continue

# Create _index.md file for namespace
target_namespace_dir = os.path.join(target_directory, namespace_parent, namespace, '_index.md')
# Create _index.md file for namespace (flattened directly under target)
target_namespace_dir = os.path.join(target_directory, namespace, '_index.md')
os.makedirs(os.path.dirname(target_namespace_dir), exist_ok=True)
with open(target_namespace_dir, 'w') as f:
f.write(hugo_template.format(namespace, namespace, namespace))
f.write(hugo_template.format(display_name(namespace), display_name(namespace)))

api_versions = os.listdir(os.path.join(source_directory, namespace_parent, namespace))
if not api_versions:
Expand All @@ -89,25 +230,35 @@
continue

# Create _index.md file for API version
target_api_version_dir = os.path.join(target_directory, namespace_parent, namespace, api_version, '_index.md')
target_api_version_dir = os.path.join(target_directory, namespace, api_version, '_index.md')
os.makedirs(os.path.dirname(target_api_version_dir), exist_ok=True)
with open(target_api_version_dir, 'w') as f:
f.write(hugo_template.format(api_version, api_version, api_version))
f.write(hugo_template.format(api_version, api_version))

resource_markdown_files = os.listdir(os.path.join(source_directory, namespace_parent, namespace, api_version, 'docs'))
if not resource_markdown_files:
print("No resource markdown files found in namespace {} and API version: {}".format(namespace, api_version))
continue

# Canonical (camelCase) resource names keyed by lowercase segment.
canonical_resource_names = load_canonical_resource_names(
os.path.join(source_directory, namespace_parent, namespace, api_version, 'types.json'))

for resource_markdown_file in resource_markdown_files:
if not resource_markdown_file.endswith(".md"):
continue

resource_name = resource_markdown_file.split(".")[0]
print("Processing resource: {}/{}@{}".format(namespace, resource_name, api_version))
target_resource_dir = os.path.join(target_directory, namespace_parent, namespace, api_version, resource_name)
# Use the canonical camelCase spelling for the resource segment
# (e.g. "bicepSettings" instead of the lowercase filename).
canonical_name = canonical_resource_names.get(resource_name.lower(), resource_name)
# PascalCase title for the sidebar/table of contents link.
link_title = canonical_name[:1].upper() + canonical_name[1:]
print("Processing resource: {}/{}@{}".format(namespace, canonical_name, api_version))
target_resource_dir = os.path.join(target_directory, namespace, api_version, resource_name)

hugo_content = hugo_template.format('{}/{}@{}'.format(namespace, resource_name, api_version), resource_name, '{}/{}@{}'.format(namespace, resource_name, api_version))
qualified_name = '{}/{}@{}'.format(display_name(namespace), canonical_name, api_version)
hugo_content = hugo_template.format(qualified_name, link_title)
hugo_content += "{{< schemaExample >}}\n\n"

## Check if a Bicep file exists for the resource
Expand All @@ -125,8 +276,7 @@

# Add in the resource markdown file
markdown_content = open(os.path.join(source_directory, namespace_parent, namespace, api_version, 'docs', resource_markdown_file), 'r').read()
hugo_content += "## Schema\n\n"
hugo_content += markdown_content
hugo_content += strip_boolean_columns(demote_description_headings(markdown_content))

# Create the target markdown file
target_markdown_file = os.path.join(target_resource_dir, 'index.md')
Expand Down
13 changes: 0 additions & 13 deletions docs/assets/scss/_code.scss
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
// Code formatting.

.highlight .copy-icon {
position: absolute;
right: 20px;
top: 18px;
opacity: 0.7;
}


.highlight pre {
/* Avoid pushing up the copy buttons. */
margin: 0;
}

.td-content {

// Highlighted code.
Expand Down
39 changes: 36 additions & 3 deletions docs/assets/scss/_content.scss
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,47 @@
@extend .img-fluid;
}

> table {
> table:not(.td-initial) {
@extend .table-striped;

@extend .table-responsive;

@extend .table-bordered;

@extend .table;

// Use a fixed layout so columns get predictable widths and don't
// collapse to fit long unbreakable tokens (e.g. resource IDs). We
// deliberately do NOT use Bootstrap's `.table-responsive` (which sets
// `display: block` and adds horizontal scrolling); instead long content
// wraps within its cell. The `:not(.td-initial)` qualifier matches the
// specificity of Docsy's own responsive-table rule so `display: table`
// wins the cascade.
display: table;
table-layout: fixed;
width: 100%;

// Long tokens like resource IDs wrap within their cell instead of
// forcing the table wide, while normal words break only at spaces.
td, th {
overflow-wrap: anywhere;
word-break: normal;
}

// Give the columns explicit widths so a long unbreakable token (e.g. a
// resource ID) in one column can't hog the table width and collapse the
// others. The last column takes the remaining space and wraps its
// content. "Description" is only pinned in 4-column tables (where it is
// not the last column); in 3-column tables it takes the remaining space.
th:first-child, td:first-child {
width: 24%;
}

th:nth-child(2), td:nth-child(2) {
width: 16%;
}

th:nth-child(3):not(:last-child), td:nth-child(3):not(:last-child) {
width: 40%;
}
}

> blockquote {
Expand Down
19 changes: 19 additions & 0 deletions docs/assets/scss/_styles_project.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,29 @@
@import "sidebar-tree";
@import "content";

// Code blocks use a dark syntax theme, so Docsy's default (dark) copy-to-clipboard
// button is nearly invisible against them until hovered. Use a light color so the
// button stays visible at all times, like other docs sites.
.td-content .highlight pre button.td-click-to-copy {
color: rgba($white, 0.6);

&:hover,
&:active {
color: $white;
background-color: rgba($white, 0.15);
}
}

.td-navbar .navbar-brand__name {
display: none;
}

// Add breathing room above the "Feedback" footer section so it isn't crowded
// against the preceding page content.
.feedback--title {
margin-top: 2rem;
}

.btn-success {
background: #3176d9;
background-color: #3176d9;
Expand Down
7 changes: 0 additions & 7 deletions docs/content/community/_index.md

This file was deleted.

Loading
Loading