DAX UDF Library AutoDocs
Auto generate docs for DAX UDF libraries
I've been working on a number of DAX UDF libraries for DAX Lib. We all know the feeling: you've been working hard on development, you've already documented the code, but the thought of having to spend another few hours creating proper documentation feels like a drag. This post is about a solution to auto-generate this documentation.
JSDoc Format¶
In SQLBI's DAX Naming Conventions, it suggests using the JSDoc standard for DAX UDF function comments.
JSDoc is a markup language used to annotate source code with documentation. Originally designed for JavaScript, the format uses structured comments with special tags to describe functions, parameters, return values, and other code elements.
Basic Structure¶
JSDoc comments start with /// (triple-slash in DAX) and use @tags to denote different documentation elements:
/// Function description goes here
/// @param {type} paramName – Parameter description
/// @returns {type} Return value description
/// @example FunctionName(arg1, arg2)
function 'Library.Function' = ...
Common Tags¶
The most frequently used JSDoc tags include:
@param– Documents function parameters with type, name, and description@returns– Describes what the function returns@example– Provides usage examples@author– Names the function author/maintainer@version– Indicates current version number@since– Records when the function was first introduced@see– References related functions or documentation@deprecated– Marks functions as obsolete with migration guidance
JSDoc's strength lies in its standardization—tools can parse these comments to automatically generate documentation, IDE tooltips, and API references, making it valuable for maintaining consistent documentation across large codebases.
EvaluationContext.Colour¶
I have a library [EvaluationContext.Colour](https://daxlib.org/package/EvaluationContext.Colour) set up as a DAX Lib Medium/Large library, and I'm hosting the documentation on GitHub Pages using Material for MkDocs. The repo has both the source functions for DAX Lib and the site definition.
Repo Structure¶
├── 📁 .devcontainer // (1)!
│ ├── 📄 devcontainer.json
│ └── 📄 Dockerfile
├── 📁 .github
│ └── 📁 workflows
│ ├── 📄 publish-package.yml // (2)!
│ └── 📄 ci.yml // (3)!
├── 📁 src // (4)!
│ ├── 📁 lib
│ │ └── functions.tmdl
│ ├── 📄 icon.png
│ ├── 📄 README.md
│ └── 📄 manifest.daxlib
├── 📁 docs // (5)!
│ ├── 📄 index.md
│ └── 📄 ...
├── 📁 PowerBI // (6)!
│ ├── 📄 Model.pbip
│ └── 📄 ...
├── 📄 mkdocs.yml // (7)!
├── 📄 requirements.txt // (8)!
└── 📄 .gitignore // (9)!
- Dev Container configuration for containerized development
- Workflow to create a pull request to
daxlib/daxlibrepo - GitHub workflow to build and deploy site to
gh-pagesbranch - DAX Lib library source
- Markdown files for all MkDocs site pages
- Power BI Project file for function testing and examples
- MkDocs site configuration
- Python dependencies for MkDocs and Material theme
- Git ignore file for PBIP and MkDocs specific files
Comments¶
Currently my comments are formatted like this:
/// Select theme colour, wrapping around to the start if variant exceeds available options
/// themeName STRING The theme name e.g. "Office", "Power BI"
/// variant INT64 The variant index (1-N, wraps around if exceeds available variants)
So first things first, we need to update them to JSDoc standards:
/// Selects a theme color with variant wrapping
/// @param {string} themeName – The theme name (e.g., "Office", "Power BI")
/// @param {int} variant – The variant index (1-N, wraps around if exceeds available variants)
/// @returns {string} Theme color in hex format
/// @example EvaluationContext.Colour.Hex.Theme("Power BI", 2) // Returns "#12239E"
function 'EvaluationContext.Colour.Hex.Theme' =
...
Solution¶
We're going to want to generate a script that will perform the following actions:
- Parse - Reads
src/lib/functions.tmdland parse JSDoc@tagfrom UDF function comments - Store - Store
@tagsas json - Enhance - Include additional content that is too verbose for inclusion in function comments but should be included in the site page
- Merge - Combines JSDoc data + extended content using a Jinja template
- Generate - Writes complete markdown documentation pages to the
docs/folder - Organize - Automatically places files in correct subfolders (conversion, hex-manipulation, themes) based on function name
Supported Tags
| Tag | Required | Format | Purpose |
|---|---|---|---|
| Description | /// Description text |
Brief function summary | |
@param |
@param {type} name – description |
Document parameters, [] for optional |
|
@returns |
@returns {type} description |
Describe return value | |
@example |
@example FunctionCall() |
Override auto-generated example | |
@author |
@author Name |
Function author/maintainer | |
@version |
@version 1.0.0 |
Current version | |
@since |
@since 1.0.0 |
Version/date introduced | |
@see |
@see Reference |
Related functions (can repeat) | |
@deprecated |
@deprecated Message |
Mark as obsolete |
update_docs.py performs the actions above, supported by some other files detailed below.
...
├── 📁 JSDoc
│ ├── 📁 extended_content
│ │ ├── 📄 Hex.Theme.pretab.md // (1)!
│ │ └── 📄 Hex.Theme.tabs.md // (2)!
│ ├── 📁 parsed_functions // (3)!
│ │ ├── 📄 EvaluationContext.Colour.Hex.AdjustAlpha.json
│ │ ├── ...
│ │ └── 📄 EvaluationContext.Colour.RGB.ToHex.json
│ ├── 📄 config.yaml // (4)!
│ ├── 📄 doc_template.md.jinja // (5)!
│ └── 📄 update_docs.py // (6)!
...
- Content that appears before the tabbed sections (Syntax/Example/Definition)
- Additional tabs that appear alongside the standard Syntax/Example/Definition tabs
- Function metadata extracted from JSDoc comments
- Config file to specify source, destination and template, and path_mapping to map functions to logical folders
- Template for how function metadata and extended content should generate markdown files
- Script to extract function metadata and generate function markdown files using Jinja template
Configuration for update_docs.py
# Documentation Generation Configuration
# REQUIRED: Source TMDL file (relative to repository root)
source: src/lib/functions.tmdl
# REQUIRED: Destination directory (relative to repository root)
destination: docs
# OPTIONAL: Jinja template file (relative to repository root)
template: JSdoc/doc_template.md.jinja
# OPTIONAL: Output directory for JSON metadata (relative to JSdoc folder)
output_dir: parsed_functions
path_mapping:
# Pattern matching: map patterns to subfolders
# Use | to separate multiple patterns for the same folder
pattern_match:
"ToHex|ToInt": conversion
"Theme|Interpolate|LinearTheme": themes
"Hex.": hex-manipulation
# Exact function name to path mapping (overrides pattern matching)
# Use relative paths from destination directory or absolute paths
function_paths:
"Hex.Theme": themes
"Hex.Interpolate": themes
"Hex.LinearTheme": themes
"RGB.ToHex": conversion
"HSL.ToHex": conversion
"Int.ToHex": conversion
"Hex.ToInt": conversion
Extra markdown context to be added to specific function markdown pages
=== "Themes"
| Variant | Power BI | Modern Corporate | Ocean Breeze | Sunset Vibes | Forest Green | Purple Rain | Monochrome | Vibrant Tech | Earth Tones | Pastel Dreams | Midnight Blue |
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| 1 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #118DFF;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #2E3440;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #0077BE;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FF6B35;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #355E3B;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #301934;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #1C1C1C;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FF0080;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #8B4513;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFB3BA;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #191970;"></span> |
| 2 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #12239E;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #3B4252;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #00A8CC;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #F7931E;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #228B22;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #663399;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #333333;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #00FFFF;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #A0522D;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFDFBA;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #000080;"></span> |
| 3 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #E66C37;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #434C5E;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #40E0D0;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFD23F;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #32CD32;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #9966CC;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #666666;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFFF00;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #CD853F;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFFFBA;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #0000CD;"></span> |
| 4 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #6B007B;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #4C566A;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #87CEEB;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #EE4B2B;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #90EE90;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #BA55D3;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #999999;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FF8000;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #DEB887;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #BAFFC9;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #4169E1;"></span> |
| 5 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #E044A7;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #5E81AC;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #B0E0E6;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #C04000;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #98FB98;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #DDA0DD;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #CCCCCC;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #8000FF;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #F4A460;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #BAE1FF;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #6495ED;"></span> |
| 6 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #744EC2;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #81A1C1;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #E0F6FF;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFCBA4;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #F0FFF0;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #E6E6FA;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #F5F5F5;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #00FF80;"></span> | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #FFF8DC;"></span> | | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #B0C4DE;"></span> |
| ... |
| 41 | <span class="d-inline-block p-2 mr-1 v-align-middle" style="background-color: #0B511F;"></span>
Jinja template for how JSON files and extended context should generate markdown pages
Script to parse function.tmdl and generate function markdown pages
import re
import json
import yaml
from pathlib import Path
from typing import Dict, List, Optional
from dataclasses import dataclass, asdict
@dataclass
class FunctionParameter:
"""Represents a function parameter."""
name: str
type: str
description: str
optional: bool = False
default_value: Optional[str] = None
@dataclass
class FunctionMetadata:
"""Represents extracted function metadata."""
name: str
short_name: str # e.g., "Hex.Theme" from "EvaluationContext.Colour.Hex.Theme"
description: str
parameters: List[FunctionParameter]
return_type: str
return_description: str
code: str
example: Optional[str] = None
author: Optional[str] = None
version: Optional[str] = None
since: Optional[str] = None
see: Optional[List[str]] = None
deprecated: Optional[str] = None
def parse_tmdl_functions(tmdl_path: Path) -> List[FunctionMetadata]:
"""
Parse TMDL file and extract function metadata from JSDoc comments.
Args:
tmdl_path: Path to the functions.tmdl file
Returns:
List of FunctionMetadata objects
"""
content = tmdl_path.read_text(encoding='utf-8')
functions = []
# Split by annotation blocks to separate functions
# Each function ends with annotation lines
function_blocks = re.split(
r'(\n\s*annotation\s+DAXLIB_PackageVersion\s*=.*?\n)',
content
)
# Process pairs: function_code + annotation
i = 0
while i < len(function_blocks):
block = function_blocks[i]
# Look for JSDoc comment + function definition
func_match = re.search(
r"((?:///.*\n)+)" # JSDoc comments
r"\s*"
r"function\s+'([^']+)'\s*=" # function 'name' =
r"(.*)", # Function body (rest of block)
block,
re.DOTALL | re.MULTILINE
)
if func_match:
jsdoc_block = func_match.group(1)
function_name = func_match.group(2).strip()
function_body = func_match.group(3).strip()
# Extract short name (e.g., "Hex.Theme" from "EvaluationContext.Colour.Hex.Theme")
short_name = function_name.replace('EvaluationContext.Colour.', '')
# Parse JSDoc comments
description_lines = []
parameters = []
return_type = ""
return_description = ""
custom_example = None # For @example tag
author = None
version = None
since = None
see_also = []
deprecated = None
for line in jsdoc_block.split('\n'):
line = line.strip().lstrip('/').strip()
if line.startswith('@param'):
# Parse: @param {type} name – description
# or: @param {type} [name] – description (optional)
# or: @param {type} [name=default] – description (optional with default)
# Try optional parameter with default value first
param_match = re.match(r'@param\s+\{([^}]+)\}\s+\[(\w+)=([^\]]+)\]\s+–\s+(.*)', line)
if param_match:
param_type = param_match.group(1)
param_name = param_match.group(2)
default_value = param_match.group(3)
param_desc = param_match.group(4)
parameters.append(FunctionParameter(param_name, param_type, param_desc, optional=True, default_value=default_value))
else:
# Try optional parameter without default value
param_match = re.match(r'@param\s+\{([^}]+)\}\s+\[(\w+)\]\s+–\s+(.*)', line)
if param_match:
param_type = param_match.group(1)
param_name = param_match.group(2)
param_desc = param_match.group(3)
parameters.append(FunctionParameter(param_name, param_type, param_desc, optional=True))
else:
# Try required parameter
param_match = re.match(r'@param\s+\{([^}]+)\}\s+(\w+)\s+–\s+(.*)', line)
if param_match:
param_type = param_match.group(1)
param_name = param_match.group(2)
param_desc = param_match.group(3)
parameters.append(FunctionParameter(param_name, param_type, param_desc))
elif line.startswith('@example'):
# Parse: @example function call or description
example_match = re.match(r'@example\s+(.*)', line)
if example_match:
custom_example = example_match.group(1)
elif line.startswith('@author'):
# Parse: @author Name
author_match = re.match(r'@author\s+(.*)', line)
if author_match:
author = author_match.group(1)
elif line.startswith('@version'):
# Parse: @version 1.0.0
version_match = re.match(r'@version\s+(.*)', line)
if version_match:
version = version_match.group(1)
elif line.startswith('@since'):
# Parse: @since 1.0.0 or @since 2024-01-01
since_match = re.match(r'@since\s+(.*)', line)
if since_match:
since = since_match.group(1)
elif line.startswith('@see'):
# Parse: @see RelatedFunction or @see https://url
see_match = re.match(r'@see\s+(.*)', line)
if see_match:
see_also.append(see_match.group(1))
elif line.startswith('@deprecated'):
# Parse: @deprecated Use NewFunction instead
deprecated_match = re.match(r'@deprecated\s+(.*)', line)
if deprecated_match:
deprecated = deprecated_match.group(1)
else:
deprecated = "This function is deprecated"
elif line.startswith('@returns'):
# Parse: @returns {type} description
return_match = re.match(r'@returns\s+\{([^}]+)\}\s+(.*)', line)
if return_match:
return_type = return_match.group(1).upper()
return_description = return_match.group(2)
elif line and not line.startswith('@'):
# Description line
description_lines.append(line.strip())
description = ' '.join(description_lines).strip()
# Extract function code (clean up formatting and remove annotations)
# Split function body to remove annotations
body_lines = function_body.split('\n')
clean_body_lines = []
for line in body_lines:
# Stop at annotation lines
if 'annotation DAXLIB_' in line:
break
clean_body_lines.append(line)
# Reconstruct the clean function body
clean_body = '\n'.join(clean_body_lines).rstrip()
# Ensure opening parenthesis has proper indentation (2 tabs)
# The function body starts with whitespace + (, we need to ensure it has 2 tabs
if clean_body.lstrip().startswith('('):
# Remove leading whitespace and add 2 tabs
clean_body = '\t\t' + clean_body.lstrip()
code = f"function '{function_name}' =\n{clean_body}"
# Generate example (use custom example if provided, otherwise auto-generate)
example = custom_example if custom_example else generate_example(function_name, parameters)
functions.append(FunctionMetadata(
name=function_name,
short_name=short_name,
description=description,
parameters=parameters,
return_type=return_type,
return_description=return_description,
code=code,
example=example,
author=author,
version=version,
since=since,
see=see_also if see_also else None,
deprecated=deprecated
))
# Move to next block
i += 1
return functions
def generate_example(function_name: str, parameters: List[FunctionParameter]) -> str:
"""
Generate a basic example usage for a function.
This is just a fallback - use @example tag for better examples.
Args:
function_name: Full function name
parameters: List of function parameters
Returns:
Example DAX code as string
"""
# Create simple placeholder example
if len(parameters) == 0:
return f"{function_name}()"
# Create generic parameter placeholders
param_names = [p.name for p in parameters]
params_str = ', '.join(param_names)
return f"{function_name}({params_str})"
def type_to_label(dax_type: str) -> str:
"""
Convert DAX type to HTML span with proper class.
Args:
dax_type: DAX type (STRING, INT64, DOUBLE, DECIMAL, etc.)
Returns:
HTML span element
"""
type_map = {
'STRING': ('string', 'STRING'),
'INT64': ('int64', 'INT64'),
'INT': ('int64', 'INT64'),
'INTEGER': ('int64', 'INT64'),
'DOUBLE': ('number', 'DOUBLE'),
'NUMBER': ('number', 'DOUBLE'),
'DECIMAL': ('number', 'DOUBLE'),
}
css_class, display_text = type_map.get(dax_type.upper(), ('string', 'STRING'))
return f'<span class="type-label {css_class}">{display_text}</span>'
def create_syntax_section(func: FunctionMetadata) -> str:
"""
Create the Syntax tab content.
Args:
func: Function metadata
Returns:
Markdown string for syntax section
"""
lines = ['=== "Syntax"', '', ' ```dax']
# Function signature - do NOT use brackets in code sample
param_parts = []
for p in func.parameters:
param_parts.append(p.name)
param_list = ', '.join(param_parts)
lines.append(f' {func.name}( {param_list} )')
lines.append(' ```')
lines.append('')
# Parameters table
if func.parameters:
lines.append(' | Parameter | Type | Required | Description |')
lines.append(' |:---:|:---:|:---:|---|')
for param in func.parameters:
type_label = type_to_label(param.type)
# Use :material-close: for optional, :material-check: for required
required_icon = ':material-close:' if param.optional else ':material-check:'
# Add default value to description if present
description = param.description
if param.optional and param.default_value:
description += f' (default: {param.default_value})'
lines.append(f' | {param.name} | {type_label} | {required_icon} | {description} |')
lines.append('')
# Return type
return_label = type_to_label(func.return_type)
lines.append(f' {return_label} {func.return_description}')
return '\n'.join(lines)
def create_example_section(func: FunctionMetadata) -> str:
"""
Create the Example tab content.
Args:
func: Function metadata
Returns:
Markdown string for example section
"""
lines = ['=== "Example"', '', ' ```dax']
lines.append(f' {func.example}')
lines.append(' ```')
return '\n'.join(lines)
def create_definition_section(func: FunctionMetadata) -> str:
"""
Create the Definition tab content.
Args:
func: Function metadata
Returns:
Markdown string for definition section
"""
lines = ['=== "Definition"', '', ' ```dax']
# Add function code with proper indentation
# Annotations already removed during parsing
for line in func.code.split('\n'):
lines.append(f' {line}')
lines.append(' ```')
return '\n'.join(lines)
def get_doc_path(base_dir: Path, func: FunctionMetadata, custom_mapping: Optional[Dict] = None) -> Path:
"""
Determine the appropriate documentation path based on function type.
Args:
base_dir: Base docs directory
func: Function metadata
custom_mapping: Optional custom mapping from config (pattern_match or function_paths)
Returns:
Path to the documentation file
"""
short_name = func.short_name
filename = f"{func.short_name}.md"
# Check custom mapping first
if custom_mapping:
# Check for exact function name match
if 'function_paths' in custom_mapping and short_name in custom_mapping['function_paths']:
custom_path = custom_mapping['function_paths'][short_name]
return Path(custom_path) if Path(custom_path).is_absolute() else base_dir / custom_path / filename
# Check pattern matching
if 'pattern_match' in custom_mapping:
for pattern, subfolder in custom_mapping['pattern_match'].items():
# Split pattern by | for OR matching
patterns = [p.strip() for p in pattern.split('|')]
if any(p in short_name for p in patterns):
return base_dir / subfolder / filename
# Default pattern matching
if any(x in short_name for x in ['ToHex', 'ToInt']):
subfolder = 'conversion'
elif any(x in short_name for x in ['Theme', 'Interpolate', 'LinearTheme']):
subfolder = 'theming'
elif short_name.startswith('Hex.'):
subfolder = 'hex-manipulation'
else:
subfolder = 'conversion' # Default
return base_dir / subfolder / filename
def save_function_metadata(output_dir: Path, func: FunctionMetadata) -> Path:
"""
Save function metadata to a JSON file for use with Jinja templates.
Args:
output_dir: Directory to save JSON files
func: Function metadata
Returns:
Path to the saved JSON file
"""
output_dir.mkdir(parents=True, exist_ok=True)
# Convert to dictionary, handling the dataclass nested structure
func_dict = {
'name': func.name,
'short_name': func.short_name,
'description': func.description,
'parameters': [
{
'name': p.name,
'type': p.type,
'type_label': type_to_label(p.type),
'description': p.description,
'optional': p.optional,
'default_value': p.default_value
}
for p in func.parameters
],
'return_type': func.return_type,
'return_type_label': type_to_label(func.return_type),
'return_description': func.return_description,
'code': func.code,
'example': func.example,
'author': func.author,
'version': func.version,
'since': func.since,
'see': func.see,
'deprecated': func.deprecated,
# Pre-rendered sections for convenience
'sections': {
'syntax': create_syntax_section(func),
'example': create_example_section(func),
'definition': create_definition_section(func)
}
}
# Save to JSON file named after the function
json_path = output_dir / f"{func.name}.json"
with json_path.open('w', encoding='utf-8') as f:
json.dump(func_dict, f, indent=2, ensure_ascii=False)
return json_path
def load_extended_content(base_dir: Path, func: FunctionMetadata, content_type: str) -> Optional[str]:
"""
Load extended content for a function if it exists.
Args:
base_dir: Base directory containing extended_content folder
func: Function metadata
content_type: Type of content ('pretab' or 'tabs')
Returns:
Content string if file exists (with trailing whitespace stripped), None otherwise
"""
extended_dir = base_dir / 'extended_content'
filename = f"{func.short_name}.{content_type}.md"
filepath = extended_dir / filename
if filepath.exists():
try:
# Read and strip trailing whitespace/newlines
return filepath.read_text(encoding='utf-8').rstrip()
except Exception as e:
print(f"Warning: Could not read {filepath}: {e}")
return None
return None
def create_page_from_template(template_path: Path, output_path: Path, func: FunctionMetadata, extended_content_dir: Optional[Path] = None) -> bool:
"""
Create a documentation page from a Jinja template.
Args:
template_path: Path to the Jinja template file
output_path: Path where the markdown file should be created
func: Function metadata
extended_content_dir: Optional directory containing extended content files
Returns:
True if created successfully
"""
try:
from jinja2 import Template
# Read template
template_content = template_path.read_text(encoding='utf-8')
template = Template(template_content)
# Load extended content if directory provided
pretab_content = None
additional_tabs = None
if extended_content_dir:
pretab_content = load_extended_content(extended_content_dir.parent, func, 'pretab')
additional_tabs = load_extended_content(extended_content_dir.parent, func, 'tabs')
# Prepare context
context = {
'name': func.name,
'short_name': func.short_name,
'description': func.description,
'parameters': func.parameters,
'return_type': func.return_type,
'return_type_label': type_to_label(func.return_type),
'return_description': func.return_description,
'code': func.code,
'example': func.example,
'syntax_section': create_syntax_section(func),
'example_section': create_example_section(func),
'definition_section': create_definition_section(func),
'pretab_content': pretab_content,
'additional_tabs': additional_tabs,
}
# Render template
rendered = template.render(**context)
# Ensure directory exists
output_path.parent.mkdir(parents=True, exist_ok=True)
# Write content
output_path.write_text(rendered, encoding='utf-8')
return True
except ImportError:
print("Warning: jinja2 not installed. Install with: pip install jinja2")
return False
except Exception as e:
print(f"Error creating page from template: {e}")
return False
def main():
"""Main execution function."""
import argparse
parser = argparse.ArgumentParser(description='Parse TMDL functions and generate documentation data')
parser.add_argument('config', type=str,
help='Path to YAML configuration file')
args = parser.parse_args()
# Load configuration from YAML
config_path = Path(args.config)
if not config_path.exists():
print(f"Error: Config file not found: {config_path}")
return
with config_path.open('r', encoding='utf-8') as f:
config = yaml.safe_load(f)
print(f"Loaded configuration from: {config_path}")
# Paths - script is now in JSdoc folder, so go up one level to root
script_dir = Path(__file__).parent
root_dir = script_dir.parent
# Get paths from config (required)
if 'source' not in config:
print("Error: 'source' not specified in config file")
return
if 'destination' not in config:
print("Error: 'destination' not specified in config file")
return
tmdl_path = root_dir / config['source']
docs_dir = root_dir / config['destination']
output_dir = script_dir / config.get('output_dir', 'parsed_functions')
template_path = config.get('template', None)
if not tmdl_path.exists():
print(f"Error: Source file not found: {tmdl_path}")
return
print(f"Parsing functions from: {tmdl_path}")
print(f"Documentation directory: {docs_dir}")
print(f"Metadata output: {output_dir}")
if template_path:
print(f"Using template: {template_path}")
# Parse functions
functions = parse_tmdl_functions(tmdl_path)
print(f"Found {len(functions)} functions")
print(f"Saving metadata to: {output_dir}")
# Extended content directory
extended_content_dir = script_dir / 'extended_content'
# Save metadata and create/update pages
saved_count = 0
created_count = 0
updated_count = 0
# Get custom path mapping from config
custom_mapping = config.get('path_mapping', None)
for func in functions:
# Save JSON metadata
json_path = save_function_metadata(output_dir, func)
saved_count += 1
print(f"\n✓ Saved metadata: {json_path.name}")
# Always create/update pages when running with config
doc_path = get_doc_path(docs_dir, func, custom_mapping)
if not doc_path.exists():
print(f" Creating page: {doc_path}")
if not template_path:
print(f" ✗ No template specified in config file")
continue
# Use Jinja template
template_file = root_dir / template_path if not Path(template_path).is_absolute() else Path(template_path)
if template_file.exists():
if create_page_from_template(template_file, doc_path, func, extended_content_dir):
created_count += 1
print(f" ✓ Created from template")
else:
print(f" ✗ Failed to create from template")
else:
print(f" ✗ Template not found: {template_file}")
else:
print(f" Updating page: {doc_path.name}")
if not template_path:
print(f" ✗ No template specified in config file")
continue
# Use Jinja template
template_file = root_dir / template_path if not Path(template_path).is_absolute() else Path(template_path)
if template_file.exists():
if create_page_from_template(template_file, doc_path, func, extended_content_dir):
updated_count += 1
print(f" ✓ Updated from template")
else:
print(f" ✗ Failed to update from template")
else:
print(f" ✗ Template not found: {template_file}")
# Create summary JSON with all functions
summary_path = output_dir / '_all_functions.json'
summary_data = {
'functions': [
{
'name': f.name,
'short_name': f.short_name,
'description': f.description,
'json_file': f"{f.name}.json"
}
for f in functions
],
'total_count': len(functions),
'categories': {
'conversion': [f.short_name for f in functions if any(x in f.short_name for x in ['ToHex', 'ToInt'])],
'hex_manipulation': [f.short_name for f in functions if f.short_name.startswith('Hex.') and not any(x in f.short_name for x in ['Theme', 'Interpolate', 'LinearTheme'])],
'themes': [f.short_name for f in functions if any(x in f.short_name for x in ['Theme', 'Interpolate', 'LinearTheme'])]
}
}
with summary_path.open('w', encoding='utf-8') as f:
json.dump(summary_data, f, indent=2, ensure_ascii=False)
print(f"\n{'='*60}")
print(f"Summary:")
print(f" Functions parsed: {len(functions)}")
print(f" Metadata files saved: {saved_count}")
print(f" New pages created: {created_count}")
print(f" Existing pages updated: {updated_count}")
print(f" Summary file: {summary_path}")
print(f"{'='*60}")
print(f"\n✓ Documentation generation complete!")
if __name__ == '__main__':
main()
Script extracts tags from function comments and stores them as a JSON file.
{
"name": "EvaluationContext.Colour.Hex.Theme",
"short_name": "Hex.Theme",
"description": "Selects a theme color with variant wrapping",
"parameters": [
{
"name": "themeName",
"type": "string",
"type_label": "<span class=\"type-label string\">STRING</span>",
"description": "The theme name (e.g., \"Office\", \"Power BI\")",
"optional": false,
"default_value": null
},
{
"name": "variant",
"type": "int",
"type_label": "<span class=\"type-label int64\">INT64</span>",
"description": "The variant index (1-N, wraps around if exceeds available variants)",
"optional": false,
"default_value": null
}
],
"return_type": "STRING",
"return_type_label": "<span class=\"type-label string\">STRING</span>",
"return_description": "Theme color in hex format",
"code": "function 'EvaluationContext.Colour.Hex.Theme' =\n\t\t(\n\t\t\tthemeName: STRING,\n\t\t\tvariant: INT64\n\t\t) =>\n\t\t\n\t\t\tVAR Themes =\n\t\t\t\tDATATABLE(\n\t\t\t\t\"ThemeName\", STRING,\n\t\t\t\t\"Variant\", INTEGER,\n\t\t\t\t\"Colour\", STRING,\n\t\t\t\t{\n\t\t\t\t\t// Power BI Default\n\t\t\t\t\t{\"Power BI\", 1, \"#118DFF\"},\n\t\t\t\t\t{\"Power BI\", 2, \"#12239E\"},\n\t\t\t\t\t{\"Power BI\", 3, \"#E66C37\"},\n\t\t\t\t\t{\"Power BI\", 4, \"#6B007B\"},\n\t\t\t\t\t{\"Power BI\", 5, \"#E044A7\"},\n\t\t\t\t\t{\"Power BI\", 6, \"#744EC2\"},\n\t\t\t\t\t{\"Power BI\", 7, \"#D9B300\"},\n\t\t\t\t\t{\"Power BI\", 8, \"#D64550\"},\n\t\t\t\t\t{\"Power BI\", 9, \"#197278\"},\n\t\t\t\t\t{\"Power BI\", 10, \"#1AAB40\"},\n\t\t\t\t\t{\"Power BI\", 11, \"#15C6F4\"},\n\t\t\t\t\t{\"Power BI\", 12, \"#4092FF\"},\n\t\t\t\t\t{\"Power BI\", 13, \"#FFA058\"},\n\t\t\t\t\t{\"Power BI\", 14, \"#BE5DC9\"},\n\t\t\t\t\t{\"Power BI\", 15, \"#F472D0\"},\n\t\t\t\t\t{\"Power BI\", 16, \"#B5A1FF\"},\n\t\t\t\t\t{\"Power BI\", 17, \"#C4A200\"},\n\t\t\t\t\t{\"Power BI\", 18, \"#FF8080\"},\n\t\t\t\t\t{\"Power BI\", 19, \"#00DBBC\"},\n\t\t\t\t\t{\"Power BI\", 20, \"#5BD667\"},\n\t\t\t\t\t{\"Power BI\", 21, \"#0091D5\"},\n\t\t\t\t\t{\"Power BI\", 22, \"#4668C5\"},\n\t\t\t\t\t{\"Power BI\", 23, \"#FF6300\"},\n\t\t\t\t\t{\"Power BI\", 24, \"#99008A\"},\n\t\t\t\t\t{\"Power BI\", 25, \"#EC008C\"},\n\t\t\t\t\t{\"Power BI\", 26, \"#533285\"},\n\t\t\t\t\t{\"Power BI\", 27, \"#99700A\"},\n\t\t\t\t\t{\"Power BI\", 28, \"#FF4141\"},\n\t\t\t\t\t{\"Power BI\", 29, \"#1F9A85\"},\n\t\t\t\t\t{\"Power BI\", 30, \"#25891C\"},\n\t\t\t\t\t{\"Power BI\", 31, \"#0057A2\"},\n\t\t\t\t\t{\"Power BI\", 32, \"#002050\"},\n\t\t\t\t\t{\"Power BI\", 33, \"#C94F0F\"},\n\t\t\t\t\t{\"Power BI\", 34, \"#450F54\"},\n\t\t\t\t\t{\"Power BI\", 35, \"#B60064\"},\n\t\t\t\t\t{\"Power BI\", 36, \"#34124F\"},\n\t\t\t\t\t{\"Power BI\", 37, \"#6A5A29\"},\n\t\t\t\t\t{\"Power BI\", 38, \"#1AAB40\"},\n\t\t\t\t\t{\"Power BI\", 39, \"#BA141A\"},\n\t\t\t\t\t{\"Power BI\", 40, \"#0C3D37\"},\n\t\t\t\t\t{\"Power BI\", 41, \"#0B511F\"},\n\t\t\n\t\t\t\t\t// Modern Corporate - Professional blues and grays\n\t\t\t\t\t{\"Modern Corporate\", 1, \"#2E3440\"},\n\t\t\t\t\t{\"Modern Corporate\", 2, \"#3B4252\"},\n\t\t\t\t\t{\"Modern Corporate\", 3, \"#434C5E\"},\n\t\t\t\t\t{\"Modern Corporate\", 4, \"#4C566A\"},\n\t\t\t\t\t{\"Modern Corporate\", 5, \"#5E81AC\"},\n\t\t\t\t\t{\"Modern Corporate\", 6, \"#81A1C1\"},\n\t\t\n\t\t\t\t\t// Ocean Breeze - Cool blues and teals\n\t\t\t\t\t{\"Ocean Breeze\", 1, \"#0077BE\"},\n\t\t\t\t\t{\"Ocean Breeze\", 2, \"#00A8CC\"},\n\t\t\t\t\t{\"Ocean Breeze\", 3, \"#40E0D0\"},\n\t\t\t\t\t{\"Ocean Breeze\", 4, \"#87CEEB\"},\n\t\t\t\t\t{\"Ocean Breeze\", 5, \"#B0E0E6\"},\n\t\t\t\t\t{\"Ocean Breeze\", 6, \"#E0F6FF\"},\n\t\t\n\t\t\t\t\t// Sunset Vibes - Warm oranges and reds\n\t\t\t\t\t{\"Sunset Vibes\", 1, \"#FF6B35\"},\n\t\t\t\t\t{\"Sunset Vibes\", 2, \"#F7931E\"},\n\t\t\t\t\t{\"Sunset Vibes\", 3, \"#FFD23F\"},\n\t\t\t\t\t{\"Sunset Vibes\", 4, \"#EE4B2B\"},\n\t\t\t\t\t{\"Sunset Vibes\", 5, \"#C04000\"},\n\t\t\t\t\t{\"Sunset Vibes\", 6, \"#FFCBA4\"},\n\t\t\n\t\t\t\t\t// Forest Green - Natural greens\n\t\t\t\t\t{\"Forest Green\", 1, \"#355E3B\"},\n\t\t\t\t\t{\"Forest Green\", 2, \"#228B22\"},\n\t\t\t\t\t{\"Forest Green\", 3, \"#32CD32\"},\n\t\t\t\t\t{\"Forest Green\", 4, \"#90EE90\"},\n\t\t\t\t\t{\"Forest Green\", 5, \"#98FB98\"},\n\t\t\t\t\t{\"Forest Green\", 6, \"#F0FFF0\"},\n\t\t\n\t\t\t\t\t// Purple Rain - Rich purples\n\t\t\t\t\t{\"Purple Rain\", 1, \"#301934\"},\n\t\t\t\t\t{\"Purple Rain\", 2, \"#663399\"},\n\t\t\t\t\t{\"Purple Rain\", 3, \"#9966CC\"},\n\t\t\t\t\t{\"Purple Rain\", 4, \"#BA55D3\"},\n\t\t\t\t\t{\"Purple Rain\", 5, \"#DDA0DD\"},\n\t\t\t\t\t{\"Purple Rain\", 6, \"#E6E6FA\"},\n\t\t\n\t\t\t\t\t// Monochrome - Sophisticated grays\n\t\t\t\t\t{\"Monochrome\", 1, \"#1C1C1C\"},\n\t\t\t\t\t{\"Monochrome\", 2, \"#333333\"},\n\t\t\t\t\t{\"Monochrome\", 3, \"#666666\"},\n\t\t\t\t\t{\"Monochrome\", 4, \"#999999\"},\n\t\t\t\t\t{\"Monochrome\", 5, \"#CCCCCC\"},\n\t\t\t\t\t{\"Monochrome\", 6, \"#F5F5F5\"},\n\t\t\n\t\t\t\t\t// Vibrant Tech - Bold and energetic\n\t\t\t\t\t{\"Vibrant Tech\", 1, \"#FF0080\"},\n\t\t\t\t\t{\"Vibrant Tech\", 2, \"#00FFFF\"},\n\t\t\t\t\t{\"Vibrant Tech\", 3, \"#FFFF00\"},\n\t\t\t\t\t{\"Vibrant Tech\", 4, \"#FF8000\"},\n\t\t\t\t\t{\"Vibrant Tech\", 5, \"#8000FF\"},\n\t\t\t\t\t{\"Vibrant Tech\", 6, \"#00FF80\"},\n\t\t\n\t\t\t\t\t// Earth Tones - Natural browns and beiges\n\t\t\t\t\t{\"Earth Tones\", 1, \"#8B4513\"},\n\t\t\t\t\t{\"Earth Tones\", 2, \"#A0522D\"},\n\t\t\t\t\t{\"Earth Tones\", 3, \"#CD853F\"},\n\t\t\t\t\t{\"Earth Tones\", 4, \"#DEB887\"},\n\t\t\t\t\t{\"Earth Tones\", 5, \"#F4A460\"},\n\t\t\t\t\t{\"Earth Tones\", 6, \"#FFF8DC\"},\n\t\t\n\t\t\t\t\t// Pastel Dreams - Soft and gentle\n\t\t\t\t\t{\"Pastel Dreams\", 1, \"#FFB3BA\"},\n\t\t\t\t\t{\"Pastel Dreams\", 2, \"#FFDFBA\"},\n\t\t\t\t\t{\"Pastel Dreams\", 3, \"#FFFFBA\"},\n\t\t\t\t\t{\"Pastel Dreams\", 4, \"#BAFFC9\"},\n\t\t\t\t\t{\"Pastel Dreams\", 5, \"#BAE1FF\"},\n\t\t\t\t\t// {\"Pastel Dreams\", 6, \"#E1BAFF\"},\n\t\t\n\t\t\t\t\t// Midnight Blue - Deep blues and navy\n\t\t\t\t\t{\"Midnight Blue\", 1, \"#191970\"},\n\t\t\t\t\t{\"Midnight Blue\", 2, \"#000080\"},\n\t\t\t\t\t{\"Midnight Blue\", 3, \"#0000CD\"},\n\t\t\t\t\t{\"Midnight Blue\", 4, \"#4169E1\"},\n\t\t\t\t\t{\"Midnight Blue\", 5, \"#6495ED\"},\n\t\t\t\t\t{\"Midnight Blue\", 6, \"#B0C4DE\"}\n\t\t\t\t}\n\t\t\t)\n\t\t\n\t\t\tVAR ThemeColors = FILTER(Themes, [ThemeName] = themeName)\n\t\t\tVAR MaxVariant = MAXX(ThemeColors, [Variant])\n\t\t\tVAR AdjustedVariant = IF(\n\t\t\t\tMaxVariant > 0,\n\t\t\t\tMOD( variant - 1, MaxVariant ) + 1,\n\t\t\t\tvariant\n\t\t\t)\n\t\t\tVAR SelectedColor =\n\t\t\t\tMAXX(\n\t\t\t\t\tFILTER( ThemeColors, [Variant] = AdjustedVariant),\n\t\t\t\t\t[Colour]\n\t\t\t\t)\n\t\t\n\t\t\tRETURN SelectedColor",
"example": "EvaluationContext.Colour.Hex.Theme(\"Power BI\", 2) // Returns \"#12239E\"",
"author": "None",
"version": null,
"since": null,
"see": null,
"deprecated": null,
"sections": {
"syntax": "=== \"Syntax\"\n\n ```dax\n EvaluationContext.Colour.Hex.Theme( themeName, variant )\n ```\n\n | Parameter | Type | Required | Description |\n |:---:|:---:|:---:|---|\n | themeName | <span class=\"type-label string\">STRING</span> | :material-check: | The theme name (e.g., \"Office\", \"Power BI\") |\n | variant | <span class=\"type-label int64\">INT64</span> | :material-check: | The variant index (1-N, wraps around if exceeds available variants) |\n\n <span class=\"type-label string\">STRING</span> Theme color in hex format",
"example": "=== \"Example\"\n\n ```dax\n EvaluationContext.Colour.Hex.Theme(\"Power BI\", 2) // Returns \"#12239E\"\n ```",
"definition": "=== \"Definition\"\n\n ```dax\n function 'EvaluationContext.Colour.Hex.Theme' =\n \t\t(\n \t\t\tthemeName: STRING,\n \t\t\tvariant: INT64\n \t\t) =>\n \t\t\n \t\t\tVAR Themes =\n \t\t\t\tDATATABLE(\n \t\t\t\t\"ThemeName\", STRING,\n \t\t\t\t\"Variant\", INTEGER,\n \t\t\t\t\"Colour\", STRING,\n \t\t\t\t{\n \t\t\t\t\t// Power BI Default\n \t\t\t\t\t{\"Power BI\", 1, \"#118DFF\"},\n \t\t\t\t\t{\"Power BI\", 2, \"#12239E\"},\n \t\t\t\t\t{\"Power BI\", 3, \"#E66C37\"},\n \t\t\t\t\t{\"Power BI\", 4, \"#6B007B\"},\n \t\t\t\t\t{\"Power BI\", 5, \"#E044A7\"},\n \t\t\t\t\t{\"Power BI\", 6, \"#744EC2\"},\n \t\t\t\t\t{\"Power BI\", 7, \"#D9B300\"},\n \t\t\t\t\t{\"Power BI\", 8, \"#D64550\"},\n \t\t\t\t\t{\"Power BI\", 9, \"#197278\"},\n \t\t\t\t\t{\"Power BI\", 10, \"#1AAB40\"},\n \t\t\t\t\t{\"Power BI\", 11, \"#15C6F4\"},\n \t\t\t\t\t{\"Power BI\", 12, \"#4092FF\"},\n \t\t\t\t\t{\"Power BI\", 13, \"#FFA058\"},\n \t\t\t\t\t{\"Power BI\", 14, \"#BE5DC9\"},\n \t\t\t\t\t{\"Power BI\", 15, \"#F472D0\"},\n \t\t\t\t\t{\"Power BI\", 16, \"#B5A1FF\"},\n \t\t\t\t\t{\"Power BI\", 17, \"#C4A200\"},\n \t\t\t\t\t{\"Power BI\", 18, \"#FF8080\"},\n \t\t\t\t\t{\"Power BI\", 19, \"#00DBBC\"},\n \t\t\t\t\t{\"Power BI\", 20, \"#5BD667\"},\n \t\t\t\t\t{\"Power BI\", 21, \"#0091D5\"},\n \t\t\t\t\t{\"Power BI\", 22, \"#4668C5\"},\n \t\t\t\t\t{\"Power BI\", 23, \"#FF6300\"},\n \t\t\t\t\t{\"Power BI\", 24, \"#99008A\"},\n \t\t\t\t\t{\"Power BI\", 25, \"#EC008C\"},\n \t\t\t\t\t{\"Power BI\", 26, \"#533285\"},\n \t\t\t\t\t{\"Power BI\", 27, \"#99700A\"},\n \t\t\t\t\t{\"Power BI\", 28, \"#FF4141\"},\n \t\t\t\t\t{\"Power BI\", 29, \"#1F9A85\"},\n \t\t\t\t\t{\"Power BI\", 30, \"#25891C\"},\n \t\t\t\t\t{\"Power BI\", 31, \"#0057A2\"},\n \t\t\t\t\t{\"Power BI\", 32, \"#002050\"},\n \t\t\t\t\t{\"Power BI\", 33, \"#C94F0F\"},\n \t\t\t\t\t{\"Power BI\", 34, \"#450F54\"},\n \t\t\t\t\t{\"Power BI\", 35, \"#B60064\"},\n \t\t\t\t\t{\"Power BI\", 36, \"#34124F\"},\n \t\t\t\t\t{\"Power BI\", 37, \"#6A5A29\"},\n \t\t\t\t\t{\"Power BI\", 38, \"#1AAB40\"},\n \t\t\t\t\t{\"Power BI\", 39, \"#BA141A\"},\n \t\t\t\t\t{\"Power BI\", 40, \"#0C3D37\"},\n \t\t\t\t\t{\"Power BI\", 41, \"#0B511F\"},\n \t\t\n \t\t\t\t\t// Modern Corporate - Professional blues and grays\n \t\t\t\t\t{\"Modern Corporate\", 1, \"#2E3440\"},\n \t\t\t\t\t{\"Modern Corporate\", 2, \"#3B4252\"},\n \t\t\t\t\t{\"Modern Corporate\", 3, \"#434C5E\"},\n \t\t\t\t\t{\"Modern Corporate\", 4, \"#4C566A\"},\n \t\t\t\t\t{\"Modern Corporate\", 5, \"#5E81AC\"},\n \t\t\t\t\t{\"Modern Corporate\", 6, \"#81A1C1\"},\n \t\t\n \t\t\t\t\t// Ocean Breeze - Cool blues and teals\n \t\t\t\t\t{\"Ocean Breeze\", 1, \"#0077BE\"},\n \t\t\t\t\t{\"Ocean Breeze\", 2, \"#00A8CC\"},\n \t\t\t\t\t{\"Ocean Breeze\", 3, \"#40E0D0\"},\n \t\t\t\t\t{\"Ocean Breeze\", 4, \"#87CEEB\"},\n \t\t\t\t\t{\"Ocean Breeze\", 5, \"#B0E0E6\"},\n \t\t\t\t\t{\"Ocean Breeze\", 6, \"#E0F6FF\"},\n \t\t\n \t\t\t\t\t// Sunset Vibes - Warm oranges and reds\n \t\t\t\t\t{\"Sunset Vibes\", 1, \"#FF6B35\"},\n \t\t\t\t\t{\"Sunset Vibes\", 2, \"#F7931E\"},\n \t\t\t\t\t{\"Sunset Vibes\", 3, \"#FFD23F\"},\n \t\t\t\t\t{\"Sunset Vibes\", 4, \"#EE4B2B\"},\n \t\t\t\t\t{\"Sunset Vibes\", 5, \"#C04000\"},\n \t\t\t\t\t{\"Sunset Vibes\", 6, \"#FFCBA4\"},\n \t\t\n \t\t\t\t\t// Forest Green - Natural greens\n \t\t\t\t\t{\"Forest Green\", 1, \"#355E3B\"},\n \t\t\t\t\t{\"Forest Green\", 2, \"#228B22\"},\n \t\t\t\t\t{\"Forest Green\", 3, \"#32CD32\"},\n \t\t\t\t\t{\"Forest Green\", 4, \"#90EE90\"},\n \t\t\t\t\t{\"Forest Green\", 5, \"#98FB98\"},\n \t\t\t\t\t{\"Forest Green\", 6, \"#F0FFF0\"},\n \t\t\n \t\t\t\t\t// Purple Rain - Rich purples\n \t\t\t\t\t{\"Purple Rain\", 1, \"#301934\"},\n \t\t\t\t\t{\"Purple Rain\", 2, \"#663399\"},\n \t\t\t\t\t{\"Purple Rain\", 3, \"#9966CC\"},\n \t\t\t\t\t{\"Purple Rain\", 4, \"#BA55D3\"},\n \t\t\t\t\t{\"Purple Rain\", 5, \"#DDA0DD\"},\n \t\t\t\t\t{\"Purple Rain\", 6, \"#E6E6FA\"},\n \t\t\n \t\t\t\t\t// Monochrome - Sophisticated grays\n \t\t\t\t\t{\"Monochrome\", 1, \"#1C1C1C\"},\n \t\t\t\t\t{\"Monochrome\", 2, \"#333333\"},\n \t\t\t\t\t{\"Monochrome\", 3, \"#666666\"},\n \t\t\t\t\t{\"Monochrome\", 4, \"#999999\"},\n \t\t\t\t\t{\"Monochrome\", 5, \"#CCCCCC\"},\n \t\t\t\t\t{\"Monochrome\", 6, \"#F5F5F5\"},\n \t\t\n \t\t\t\t\t// Vibrant Tech - Bold and energetic\n \t\t\t\t\t{\"Vibrant Tech\", 1, \"#FF0080\"},\n \t\t\t\t\t{\"Vibrant Tech\", 2, \"#00FFFF\"},\n \t\t\t\t\t{\"Vibrant Tech\", 3, \"#FFFF00\"},\n \t\t\t\t\t{\"Vibrant Tech\", 4, \"#FF8000\"},\n \t\t\t\t\t{\"Vibrant Tech\", 5, \"#8000FF\"},\n \t\t\t\t\t{\"Vibrant Tech\", 6, \"#00FF80\"},\n \t\t\n \t\t\t\t\t// Earth Tones - Natural browns and beiges\n \t\t\t\t\t{\"Earth Tones\", 1, \"#8B4513\"},\n \t\t\t\t\t{\"Earth Tones\", 2, \"#A0522D\"},\n \t\t\t\t\t{\"Earth Tones\", 3, \"#CD853F\"},\n \t\t\t\t\t{\"Earth Tones\", 4, \"#DEB887\"},\n \t\t\t\t\t{\"Earth Tones\", 5, \"#F4A460\"},\n \t\t\t\t\t{\"Earth Tones\", 6, \"#FFF8DC\"},\n \t\t\n \t\t\t\t\t// Pastel Dreams - Soft and gentle\n \t\t\t\t\t{\"Pastel Dreams\", 1, \"#FFB3BA\"},\n \t\t\t\t\t{\"Pastel Dreams\", 2, \"#FFDFBA\"},\n \t\t\t\t\t{\"Pastel Dreams\", 3, \"#FFFFBA\"},\n \t\t\t\t\t{\"Pastel Dreams\", 4, \"#BAFFC9\"},\n \t\t\t\t\t{\"Pastel Dreams\", 5, \"#BAE1FF\"},\n \t\t\t\t\t// {\"Pastel Dreams\", 6, \"#E1BAFF\"},\n \t\t\n \t\t\t\t\t// Midnight Blue - Deep blues and navy\n \t\t\t\t\t{\"Midnight Blue\", 1, \"#191970\"},\n \t\t\t\t\t{\"Midnight Blue\", 2, \"#000080\"},\n \t\t\t\t\t{\"Midnight Blue\", 3, \"#0000CD\"},\n \t\t\t\t\t{\"Midnight Blue\", 4, \"#4169E1\"},\n \t\t\t\t\t{\"Midnight Blue\", 5, \"#6495ED\"},\n \t\t\t\t\t{\"Midnight Blue\", 6, \"#B0C4DE\"}\n \t\t\t\t}\n \t\t\t)\n \t\t\n \t\t\tVAR ThemeColors = FILTER(Themes, [ThemeName] = themeName)\n \t\t\tVAR MaxVariant = MAXX(ThemeColors, [Variant])\n \t\t\tVAR AdjustedVariant = IF(\n \t\t\t\tMaxVariant > 0,\n \t\t\t\tMOD( variant - 1, MaxVariant ) + 1,\n \t\t\t\tvariant\n \t\t\t)\n \t\t\tVAR SelectedColor =\n \t\t\t\tMAXX(\n \t\t\t\t\tFILTER( ThemeColors, [Variant] = AdjustedVariant),\n \t\t\t\t\t[Colour]\n \t\t\t\t)\n \t\t\n \t\t\tRETURN SelectedColor\n ```"
}
}
Auto Generating Documentation Pages¶
Once we have the configuration set up, we can invoke the script to auto-generate our function markdown files. It will create new pages if they don't exist and overwrite existing pages.
Then we build the site to ensure everything looks as expected.
Closing Thoughts¶
The only extra parts this automation doesn't cover are index.md pages for section folders (i.e themes/index.md) and setting up site navigation in mkdocs.yml. Hopefully this template repo will help others build documentation sites for medium/large DAX Lib libraries a bit more easily.
