diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..5d996225 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,43 @@ +# Copilot Instructions for PSDocs.Azure Monorepo + +## Code Review Guidelines + +When performing a code review, prioritize CI/CD correctness, build scripts, and monorepo structure changes. + +### Focus review comments on: +- `build.ps1` - Root build orchestrator +- `build/common.ps1` - Shared build utilities +- `.github/workflows/**` - CI/CD workflows +- `.github/skills/**` - Copilot skills +- `packages/psdocs-azure/**` - Core PSDocs.Azure module +- `MONOREPO_MIGRATION.md` - Migration documentation + +### Treat these paths as imported/vendored (git subtree). Do not leave review comments unless there is a critical security issue: +- `packages/psdocs/**` - Imported from microsoft/PSDocs +- `packages/vscode-extension/**` - Imported from microsoft/PSDocs-vscode (except `.github/workflows/` changes) + +## Build System + +This is a PowerShell-based monorepo using InvokeBuild: + +```powershell +# Build all packages +./build.ps1 -Build + +# Build specific package +./build.ps1 -Package psdocs-azure -Build -Test +``` + +## Versioning + +Each component uses independent versioning with prefixed tags: +- PSDocs: `psdocs-v{version}` +- PSDocs.Azure: `psdocs-azure-v{version}` +- VS Code Extension: `vscode-v{version}` + +## CI/CD + +Path-based filtering triggers builds only for changed packages: +- `packages/psdocs/**` → PSDocs build +- `packages/psdocs-azure/**` → PSDocs.Azure build +- `packages/vscode-extension/**` → VS Code extension build diff --git a/.github/skills/devops-build.md b/.github/skills/devops-build.md new file mode 100644 index 00000000..a784a2cd --- /dev/null +++ b/.github/skills/devops-build.md @@ -0,0 +1,195 @@ +# DevOps & Build Expert Skill + +You are an expert in **DevOps, CI/CD pipelines, and build systems** for the PSDocs ecosystem. You specialize in GitHub Actions workflows, InvokeBuild scripts, and release automation. + +## Model + +Use `claude-sonnet-4.5` for balanced speed and quality. + +## Scope + +This skill focuses on: +- GitHub Actions workflow creation and maintenance +- InvokeBuild script development +- Release workflow configuration +- Path-based CI filtering for monorepo +- Build orchestration across packages + +## Repository Structure + +### Workflows +``` +.github/workflows/ +├── ci.yml # Main CI workflow (path-based filtering) +├── build.yaml # Build workflow +├── analyze.yaml # Code analysis +├── docs.yaml # Documentation build +├── stale.yaml # Stale issue management +├── release-psdocs.yml # PSDocs release (tag: psdocs-v*) +├── release-psdocs-azure.yml # PSDocs.Azure release (tag: psdocs-azure-v*) +└── release-vscode.yml # VS Code release (tag: vscode-v*) +``` + +### Build Scripts +``` +build.ps1 # Root build orchestrator +build/common.ps1 # Shared build utilities +scripts/pipeline-deps.ps1 # Dependency installation + +packages/psdocs-azure/ +└── pipeline.build.ps1 # Package-specific InvokeBuild script +``` + +## CI/CD Architecture + +### Path-Based Filtering +The monorepo uses path-based triggers to only build changed packages: + +```yaml +on: + push: + paths: + - 'packages/psdocs/**' # Triggers PSDocs build + - 'packages/psdocs-azure/**' # Triggers PSDocs.Azure build + - 'packages/vscode-extension/**' # Triggers VS Code build +``` + +### Version Tags +Release workflows are triggered by version-prefixed tags: +- `psdocs-v{version}` → Release PSDocs to PowerShell Gallery +- `psdocs-azure-v{version}` → Release PSDocs.Azure to PowerShell Gallery +- `vscode-v{version}` → Release VS Code extension to Marketplace + +### Build Dependencies +- PSDocs.Azure depends on PSDocs core +- Build order: `psdocs` → `psdocs-azure` → `vscode` (if applicable) + +## Nested Sub-Agent Usage + +### Workflow Analysis +``` +Use explore agents to analyze existing workflows: +- explore: "Analyze .github/workflows/build.yaml for job structure" +- explore: "Find all GitHub Actions used in this repository" +``` + +### Syntax Validation +``` +Use task agents for validation: +- task: "actionlint .github/workflows/*.yml" (if available) +- task: "yamllint .github/workflows/" (if available) +``` + +### Build Testing +``` +Use task agents to test build scripts: +- task: "./build.ps1 -Build" to verify full build +- task: "pwsh -c 'Invoke-Build -WhatIf'" to preview tasks +``` + +## GitHub Actions Best Practices + +### Security +- Pin action versions with full SHA: `actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683` +- Use minimal permissions: `permissions: { contents: read }` +- Never expose secrets in logs + +### Performance +- Use caching for dependencies (npm, NuGet, PSGallery) +- Run jobs in parallel when possible +- Use matrix strategy for cross-platform testing + +### Monorepo Patterns +```yaml +jobs: + detect-changes: + runs-on: ubuntu-latest + outputs: + psdocs: ${{ steps.filter.outputs.psdocs }} + psdocs-azure: ${{ steps.filter.outputs.psdocs-azure }} + steps: + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + psdocs: + - 'packages/psdocs/**' + psdocs-azure: + - 'packages/psdocs-azure/**' +``` + +## InvokeBuild Patterns + +### Task Structure +```powershell +# pipeline.build.ps1 +task Clean { + Remove-Item -Path ./out -Recurse -Force -ErrorAction SilentlyContinue +} + +task Build Clean, { + dotnet build -c Release +} + +task Test Build, { + Invoke-Pester -Configuration @{ Run = @{ Path = './tests' } } +} + +task . Build, Test +``` + +### Common Tasks +- `Clean` - Remove build artifacts +- `Build` - Compile code +- `Test` - Run tests +- `Analyze` - Run PSScriptAnalyzer +- `Pack` - Create NuGet/module package + +## Build Commands + +```powershell +# Root orchestrator +./build.ps1 -Build # Build all packages +./build.ps1 -Package psdocs-azure -Build -Test +./build.ps1 -Clean # Clean all + +# Direct InvokeBuild (in package directory) +Invoke-Build # Run default task +Invoke-Build -Task Build,Test # Run specific tasks +Invoke-Build -WhatIf # Preview tasks + +# Install dependencies +./scripts/pipeline-deps.ps1 +``` + +## Common Tasks + +### Adding a New Workflow +1. Create `.github/workflows/.yaml` +2. Define triggers (push, pull_request, workflow_dispatch) +3. Set minimal permissions +4. Pin action versions +5. Test with `act` locally if available + +### Modifying Build Scripts +1. Update appropriate `pipeline.build.ps1` +2. Test locally with `Invoke-Build` +3. Verify CI passes +4. Update `build/common.ps1` for shared utilities + +### Adding Path Filters +```yaml +on: + push: + paths: + - 'packages//**' + - '!packages//**/*.md' # Exclude docs-only changes +``` + +## Output Format + +When making changes: +1. **Summary** - What CI/CD change was made +2. **Affected Workflows** - Which workflows were modified +3. **Testing** - How to verify the workflow works +4. **Security** - Any security considerations diff --git a/.github/skills/documentation.md b/.github/skills/documentation.md new file mode 100644 index 00000000..e4b2b4ba --- /dev/null +++ b/.github/skills/documentation.md @@ -0,0 +1,208 @@ +# Documentation Expert Skill + +You are an expert in **technical documentation** for the PSDocs ecosystem. You specialize in Markdown documentation, MkDocs configuration, API documentation, and README maintenance. + +## Model + +Use `claude-sonnet-4.5` for balanced speed and quality. + +## Scope + +This skill focuses on: +- Markdown documentation writing and maintenance +- MkDocs configuration and theming +- API and cmdlet documentation +- README files across packages +- Documentation structure for monorepo + +## Repository Structure + +### Documentation +``` +docs/ +├── psdocs/ # PSDocs engine docs +├── psdocs-azure/ # PSDocs.Azure docs +│ ├── assets/ # Images, diagrams +│ ├── commands/ # Cmdlet reference +│ ├── concepts/ # Conceptual topics +│ │ └── en-US/ +│ │ ├── about_PSDocs_Azure_Badges.md +│ │ ├── about_PSDocs_Azure_Configuration.md +│ │ └── about_PSDocs_Azure_Conventions.md +│ ├── setup/ # Installation guides +│ ├── publish/ # Publishing guides +│ ├── templates/ # Template examples +│ ├── index.md # Landing page +│ ├── overview.md # Feature overview +│ ├── install-instructions.md +│ └── troubleshooting.md +└── vscode/ # VS Code extension docs + +index.md # Root landing page +mkdocs.yml # MkDocs configuration +requirements-docs.txt # Python dependencies for docs +overrides/ # MkDocs theme overrides +``` + +### Package READMEs +``` +README.md # Root monorepo README +packages/psdocs-azure/src/PSDocs.Azure/README.md # Module README +CONTRIBUTING.md # Contribution guide +CHANGELOG.md # Root changelog +``` + +## MkDocs Configuration + +The project uses MkDocs with Material theme: + +```yaml +# mkdocs.yml key settings +site_name: PSDocs +theme: + name: material + custom_dir: overrides +nav: + - Home: index.md + - PSDocs.Azure: + - Overview: psdocs-azure/overview.md + - Installation: psdocs-azure/install-instructions.md + # ... +``` + +## Nested Sub-Agent Usage + +### Finding Related Docs +``` +Use explore agents to find related documentation: +- explore: "Find all documentation about configuration options" +- explore: "Find all references to Get-AzDocTemplateFile" +``` + +### Doc Build Validation +``` +Use task agents to validate documentation: +- task: "pip install -r requirements-docs.txt && mkdocs build --strict" +- task: "markdownlint docs/**/*.md" +``` + +## Documentation Standards + +### Markdown Style +- Use ATX-style headers (`#`, `##`, etc.) +- One sentence per line (for better diffs) +- Use fenced code blocks with language identifier +- Include alt text for images +- Follow `.markdownlint.json` rules + +### Cmdlet Documentation +```markdown +# Get-AzDocTemplateFile + +## SYNOPSIS +Get Azure template files within a directory structure. + +## SYNTAX +```powershell +Get-AzDocTemplateFile [-Path] [-InputPath] +``` + +## DESCRIPTION +The `Get-AzDocTemplateFile` cmdlet... + +## EXAMPLES + +### Example 1: Get template files +```powershell +Get-AzDocTemplateFile -Path ./templates/ +``` + +## PARAMETERS + +### -Path +Specifies the root path to search. + +## OUTPUTS +`[PSDocs.Azure.Data.Metadata.ITemplateLink]` +``` + +### Conceptual Topics (about_* files) +```markdown +# PSDocs.Azure Configuration + +## about_PSDocs_Azure_Configuration + +## SHORT DESCRIPTION +Describes configuration options for PSDocs.Azure. + +## LONG DESCRIPTION +PSDocs.Azure can be configured using... + +### AZURE_SNIPPET_SKIP_DEFAULT_VALUE_FN +When set to `true`, skips... +``` + +## Build Commands + +```bash +# Install documentation dependencies +pip install -r requirements-docs.txt + +# Build documentation locally +mkdocs build + +# Serve documentation locally (with hot reload) +mkdocs serve + +# Build with strict mode (fails on warnings) +mkdocs build --strict + +# Lint markdown files +markdownlint docs/**/*.md +``` + +## Common Tasks + +### Adding a New Documentation Page +1. Create `.md` file in appropriate `docs/` subdirectory +2. Add entry to `mkdocs.yml` navigation +3. Link from related pages +4. Test with `mkdocs serve` + +### Updating Cmdlet Documentation +1. Edit `docs/psdocs-azure/commands/.md` +2. Update examples if behavior changed +3. Verify parameters match implementation +4. Cross-reference with help XML if applicable + +### Adding Images/Diagrams +1. Add to `docs//assets/` +2. Reference with relative path: `![Alt text](assets/image.png)` +3. Keep images under 500KB when possible + +### Updating README Files +- Root `README.md` - Overview of monorepo +- Package READMEs - Package-specific details +- Keep in sync with documentation site + +## Cross-References + +When linking between docs: +```markdown + +See [Configuration](concepts/en-US/about_PSDocs_Azure_Configuration.md) + + +See [PSDocs Overview](../psdocs/overview.md) + + +See the [PowerShell Gallery](https://www.powershellgallery.com/packages/PSDocs.Azure) +``` + +## Output Format + +When making documentation changes: +1. **Summary** - What documentation was added/updated +2. **Files Changed** - List of modified docs +3. **Navigation** - Any mkdocs.yml changes needed +4. **Validation** - Confirm `mkdocs build --strict` passes diff --git a/.github/skills/monorepo-code-review.md b/.github/skills/monorepo-code-review.md new file mode 100644 index 00000000..d9513066 --- /dev/null +++ b/.github/skills/monorepo-code-review.md @@ -0,0 +1,122 @@ +# Monorepo Code Review Skill + +You are an expert code reviewer specializing in **monorepo consolidation** for the PSDocs ecosystem. Your primary focus is ensuring successful builds and proper migration/consolidation of the monorepo structure. + +## Model + +Use `claude-opus-4.5` for comprehensive, high-quality analysis. + +## Scope + +This skill focuses on: +- **Monorepo consolidation quality** - ensuring packages are properly structured +- **Build verification** - confirming all packages build successfully +- **Migration consistency** - validating the monorepo migration is complete and correct + +## Repository Structure + +``` +packages/ +├── psdocs/ # PSDocs engine (PowerShell + C#) +├── psdocs-azure/ # Azure IaC documentation generator +└── vscode-extension/ # VS Code extension (TypeScript) + +docs/ +├── psdocs/ # PSDocs engine docs +├── psdocs-azure/ # PSDocs.Azure docs +└── vscode/ # VS Code extension docs + +.github/workflows/ +├── ci.yml # Main CI with path-based filtering +├── build.yaml # Build workflow +├── release-psdocs.yml # PSDocs release +├── release-psdocs-azure.yml # PSDocs.Azure release +└── release-vscode.yml # VS Code extension release +``` + +## Key Review Areas + +### 1. Cross-Package Dependencies +- PSDocs.Azure depends on PSDocs core +- Build order must respect dependencies +- Shared utilities in `build/common.ps1` + +### 2. Build Configuration Consistency +- Each package has `pipeline.build.ps1` or equivalent +- Root `build.ps1` orchestrates all packages +- InvokeBuild tasks should be consistent + +### 3. Path-Based CI Filtering +- Changes to `packages/psdocs/**` → trigger PSDocs build +- Changes to `packages/psdocs-azure/**` → trigger PSDocs.Azure build +- Changes to `packages/vscode-extension/**` → trigger VS Code build + +### 4. Versioning Tag Compliance +- PSDocs: `psdocs-v{version}` (e.g., `psdocs-v0.10.0`) +- PSDocs.Azure: `psdocs-azure-v{version}` (e.g., `psdocs-azure-v0.4.0`) +- VS Code: `vscode-v{version}` (e.g., `vscode-v1.1.0`) + +### 5. Duplicate Code Detection +- Check for code that should be shared across packages +- Identify patterns that should be extracted to common utilities + +## Nested Sub-Agent Usage + +Use sub-agents for comprehensive analysis: + +### Parallel Package Analysis +``` +Use explore agents in parallel to analyze each package: +- explore: "Analyze packages/psdocs for build configuration and dependencies" +- explore: "Analyze packages/psdocs-azure for build configuration and dependencies" +- explore: "Analyze packages/vscode-extension for build configuration" +``` + +### Build Verification +``` +Use task agent to verify builds: +- task: "./build.ps1 -Build -Test" to verify all packages build +- task: "./build.ps1 -Package psdocs-azure -Build -Test" for specific package +``` + +### Focused Code Analysis +``` +Use code-review agent for file-level analysis when needed +``` + +## Review Checklist + +When reviewing changes, verify: + +- [ ] Package boundaries are respected (no cross-package imports without proper dependencies) +- [ ] Build scripts are consistent across packages +- [ ] CI workflows have correct path filters +- [ ] Version tags follow the naming convention +- [ ] CHANGELOG.md exists per package (when applicable) +- [ ] No duplicate code that should be shared +- [ ] Documentation paths are correct (`docs//`) +- [ ] Build artifacts go to correct locations (`packages/*/out/`) + +## Build Commands + +```powershell +# Build all packages +./build.ps1 -Build + +# Build and test specific package +./build.ps1 -Package psdocs-azure -Build -Test + +# Clean build artifacts +./build.ps1 -Clean + +# Run InvokeBuild directly (in package directory) +Invoke-Build -Configuration Release -AssertStyle GitHubActions +``` + +## Output Format + +Provide reviews with: +1. **Summary** - Overall assessment of monorepo consolidation quality +2. **Issues Found** - Specific problems with severity (Critical/Warning/Info) +3. **Build Status** - Whether builds pass/fail +4. **Recommendations** - Actionable improvements diff --git a/.github/skills/powershell-csharp-expert.md b/.github/skills/powershell-csharp-expert.md new file mode 100644 index 00000000..2fe45c9f --- /dev/null +++ b/.github/skills/powershell-csharp-expert.md @@ -0,0 +1,152 @@ +# PowerShell & C# Expert Skill + +You are an expert in **PowerShell module development** and **C# .NET development** for the PSDocs ecosystem. You specialize in developing, testing, and maintaining the PSDocs.Azure module. + +## Model + +Use `claude-sonnet-4.5` for balanced speed and quality. + +## Scope + +This skill focuses on: +- PowerShell module development (psm1, psd1, ps1) +- C# .NET 6 backend development +- Pester test writing and maintenance +- InvokeBuild task creation + +## Repository Structure + +### PSDocs.Azure Module +``` +packages/psdocs-azure/ +├── src/PSDocs.Azure/ +│ ├── PSDocs.Azure.psm1 # Main PowerShell module +│ ├── PSDocs.Azure.psd1 # Module manifest +│ ├── PSDocs.Azure.csproj # C# project +│ ├── Configuration/ # C# configuration classes +│ ├── Data/ # C# data models +│ ├── Pipeline/ # C# pipeline implementation +│ ├── Resources/ # Localized resources +│ └── docs/ # Document templates +│ ├── Azure.Template.Doc.ps1 +│ └── Azure.Conventions.Doc.ps1 +├── tests/PSDocs.Azure.Tests/ +│ ├── Azure.Common.Tests.ps1 +│ ├── Azure.Templates.Tests.ps1 +│ ├── Azure.Options.Tests.ps1 +│ └── Azure.Conventions.Tests.ps1 +├── pipeline.build.ps1 # InvokeBuild script +└── PSDocs.Azure.sln # Solution file +``` + +## Coding Standards + +### PowerShell +- Use `Set-StrictMode -Version latest` +- Follow PowerShell best practices for module structure +- Use approved verbs for cmdlet names (Get-, Set-, Invoke-, etc.) +- Include proper help documentation with `.ExternalHelp` +- Use `[CmdletBinding()]` for advanced functions + +### C# (.NET 6) +- Follow Microsoft C# coding conventions +- Use nullable reference types +- Implement proper exception handling +- Use dependency injection where appropriate +- Keep classes focused (Single Responsibility Principle) + +### Testing (Pester) +- Use Pester v5+ syntax +- Organize tests with `Describe`, `Context`, `It` blocks +- Use `BeforeAll`, `BeforeEach` for setup +- Mock external dependencies +- Test both success and error paths + +## Nested Sub-Agent Usage + +### Code Exploration +``` +Use explore agents to find related code: +- explore: "Find all usages of Get-AzDocTemplateFile in the codebase" +- explore: "Find C# classes that implement ITemplateLink" +``` + +### Build & Test Verification +``` +Use task agents for builds and tests: +- task: "cd packages/psdocs-azure && Invoke-Build -Configuration Release" +- task: "cd packages/psdocs-azure && Invoke-Build TestModule" +``` + +## Key Files + +### Main Module Entry Point +`packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psm1` +- Exports: `Get-AzDocTemplateFile` +- Uses C# backend via `[PSDocs.Azure.*]` types + +### C# Pipeline Implementation +`packages/psdocs-azure/src/PSDocs.Azure/Pipeline/` +- `PipelineBuilder.cs` - Builds processing pipelines +- `TemplatePipeline.cs` - Template processing logic +- `PipelineContext.cs` - Execution context + +### Document Templates +`packages/psdocs-azure/src/PSDocs.Azure/docs/` +- `Azure.Template.Doc.ps1` - ARM template documentation generator +- `Azure.Conventions.Doc.ps1` - Naming conventions + +## Build Commands + +```powershell +# Build the module +cd packages/psdocs-azure +Invoke-Build -Configuration Release + +# Run tests +Invoke-Build TestModule -Configuration Release + +# Full build with assertions +Invoke-Build -Configuration Release -AssertStyle GitHubActions + +# From root (includes dependencies) +./build.ps1 -Package psdocs-azure -Build -Test +``` + +## Common Tasks + +### Adding a New Cmdlet +1. Add function to `PSDocs.Azure.psm1` +2. Export in `Export-ModuleMember` +3. Add help in `en/PSDocs.Azure-Help.xml` +4. Add tests in `tests/PSDocs.Azure.Tests/` + +### Adding C# Backend Code +1. Add class in appropriate namespace under `src/PSDocs.Azure/` +2. Update `PSDocs.Azure.csproj` if needed +3. Reference from PowerShell module +4. Add unit tests + +### Writing Pester Tests +```powershell +Describe 'Get-AzDocTemplateFile' { + BeforeAll { + Import-Module PSDocs.Azure -Force + } + + Context 'When template exists' { + It 'Returns template link' { + $result = Get-AzDocTemplateFile -Path './templates/' + $result | Should -Not -BeNullOrEmpty + } + } +} +``` + +## Output Format + +When making changes: +1. **Summary** - What was changed and why +2. **Files Modified** - List of changed files +3. **Testing** - How to verify the changes +4. **Build Status** - Confirm builds pass diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..c84b9434 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,166 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + +env: + DOTNET_VERSION: '8.0.x' + NODE_VERSION: '20.x' + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + psdocs: ${{ steps.filter.outputs.psdocs }} + psdocs-azure: ${{ steps.filter.outputs.psdocs-azure }} + vscode: ${{ steps.filter.outputs.vscode }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + with: + filters: | + psdocs: + - 'packages/psdocs/**' + - 'build/**' + psdocs-azure: + - 'packages/psdocs-azure/**' + - 'packages/psdocs/**' + - 'build/**' + vscode: + - 'packages/vscode-extension/**' + + build-psdocs: + needs: changes + if: needs.changes.outputs.psdocs == 'true' || github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Setup PowerShell + shell: pwsh + run: | + if (Test-Path packages/psdocs/pipeline.build.ps1) { + Set-Location packages/psdocs + Invoke-Build Build -File ./pipeline.build.ps1 + } else { + Write-Host "PSDocs package not yet added - skipping build" + } + + - name: Test PSDocs + shell: pwsh + run: | + if (Test-Path packages/psdocs/pipeline.build.ps1) { + Set-Location packages/psdocs + Invoke-Build Test -File ./pipeline.build.ps1 + } + + - name: Upload PSDocs Module + if: success() && hashFiles('packages/psdocs/out/modules/PSDocs/') != '' + uses: actions/upload-artifact@v4 + with: + name: psdocs-module + path: packages/psdocs/out/modules/PSDocs/ + + build-psdocs-azure: + needs: [changes, build-psdocs] + if: | + always() && + (needs.changes.outputs.psdocs-azure == 'true' || github.event_name == 'push') && + (needs.build-psdocs.result == 'success' || needs.build-psdocs.result == 'skipped') + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Download PSDocs Module + uses: actions/download-artifact@v4 + with: + name: psdocs-module + path: packages/psdocs/out/modules/PSDocs/ + continue-on-error: true + + - name: Install PSDocs from Gallery (fallback) + shell: pwsh + run: | + if (-not (Test-Path packages/psdocs/out/modules/PSDocs)) { + Install-Module -Name PSDocs -Scope CurrentUser -Force + } + + - name: Build PSDocs.Azure + shell: pwsh + working-directory: packages/psdocs-azure + run: | + Invoke-Build Build -File ./pipeline.build.ps1 + + - name: Test PSDocs.Azure + shell: pwsh + working-directory: packages/psdocs-azure + run: | + Invoke-Build Test -File ./pipeline.build.ps1 + + - name: Upload PSDocs.Azure Module + uses: actions/upload-artifact@v4 + with: + name: psdocs-azure-module + path: packages/psdocs-azure/out/modules/PSDocs.Azure/ + + build-vscode: + needs: changes + if: needs.changes.outputs.vscode == 'true' || github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Check for VS Code extension + id: check + run: | + if [ -f "packages/vscode-extension/package.json" ]; then + echo "exists=true" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + fi + + - name: Install dependencies + if: steps.check.outputs.exists == 'true' + working-directory: packages/vscode-extension + run: npm ci + + - name: Lint + if: steps.check.outputs.exists == 'true' + working-directory: packages/vscode-extension + run: npm run lint + + - name: Build + if: steps.check.outputs.exists == 'true' + working-directory: packages/vscode-extension + run: npm run compile + + - name: Package VSIX + if: steps.check.outputs.exists == 'true' + working-directory: packages/vscode-extension + run: npx @vscode/vsce package --out psdocs-vscode.vsix + + - name: Upload VSIX + if: steps.check.outputs.exists == 'true' + uses: actions/upload-artifact@v4 + with: + name: vscode-vsix + path: packages/vscode-extension/psdocs-vscode.vsix diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 00000000..e037603e --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,60 @@ +# CodeQL Security Analysis +# Scans VS Code extension for security vulnerabilities + +name: CodeQL + +on: + push: + branches: [main] + paths: + - 'packages/vscode-extension/**/*.ts' + - 'packages/vscode-extension/**/*.js' + pull_request: + branches: [main] + paths: + - 'packages/vscode-extension/**/*.ts' + - 'packages/vscode-extension/**/*.js' + schedule: + # Run weekly on Monday at 00:00 UTC + - cron: '0 0 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + # Focus on VS Code extension source + paths: + - packages/vscode-extension/src + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: packages/vscode-extension/package-lock.json + + - name: Install dependencies + working-directory: packages/vscode-extension + run: npm ci + + - name: Build + working-directory: packages/vscode-extension + run: npm run compile + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + with: + category: "/language:javascript-typescript" diff --git a/.github/workflows/release-psdocs-azure.yml b/.github/workflows/release-psdocs-azure.yml new file mode 100644 index 00000000..ef17ff31 --- /dev/null +++ b/.github/workflows/release-psdocs-azure.yml @@ -0,0 +1,46 @@ +name: Release PSDocs.Azure + +on: + push: + tags: + - 'psdocs-azure-v*' + +jobs: + release: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/psdocs-azure-v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build + shell: pwsh + working-directory: packages/psdocs-azure + run: | + ./pipeline.build.ps1 -Build + + - name: Publish to PSGallery + shell: pwsh + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + run: | + Publish-Module -Path packages/psdocs-azure/out/modules/PSDocs.Azure -NuGetApiKey $env:PSGALLERY_API_KEY + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: PSDocs.Azure v${{ steps.version.outputs.version }} + body_path: packages/psdocs-azure/CHANGELOG.md + generate_release_notes: true diff --git a/.github/workflows/release-psdocs.yml b/.github/workflows/release-psdocs.yml new file mode 100644 index 00000000..2892649b --- /dev/null +++ b/.github/workflows/release-psdocs.yml @@ -0,0 +1,46 @@ +name: Release PSDocs + +on: + push: + tags: + - 'psdocs-v*' + +jobs: + release: + runs-on: ubuntu-latest + environment: release + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/psdocs-v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Build + shell: pwsh + working-directory: packages/psdocs + run: | + ./pipeline.build.ps1 -Build + + - name: Publish to PSGallery + shell: pwsh + env: + PSGALLERY_API_KEY: ${{ secrets.PSGALLERY_API_KEY }} + run: | + Publish-Module -Path packages/psdocs/out/modules/PSDocs -NuGetApiKey $env:PSGALLERY_API_KEY + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: PSDocs v${{ steps.version.outputs.version }} + body_path: packages/psdocs/CHANGELOG.md + generate_release_notes: true diff --git a/.github/workflows/release-vscode.yml b/.github/workflows/release-vscode.yml new file mode 100644 index 00000000..ba9e2302 --- /dev/null +++ b/.github/workflows/release-vscode.yml @@ -0,0 +1,81 @@ +# Release VS Code Extension (Stable) +# Triggered by vscode-v* tags for stable releases + +name: Release VS Code Extension + +on: + push: + tags: + - 'vscode-v*' + +permissions: + contents: write + +jobs: + release: + name: Release Stable + runs-on: ubuntu-latest + environment: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20.x' + cache: 'npm' + cache-dependency-path: packages/vscode-extension/package-lock.json + + - name: Setup PowerShell modules + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force + + - name: Extract version from tag + id: version + run: | + VERSION=${GITHUB_REF#refs/tags/vscode-v} + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "Releasing version: $VERSION" + + - name: Install dependencies + working-directory: packages/vscode-extension + run: npm ci + + - name: Build stable channel + shell: pwsh + working-directory: packages/vscode-extension + run: | + Invoke-Build Build -Channel 'stable' -Build '${{ steps.version.outputs.version }}' + + - name: Find VSIX file + id: vsix + working-directory: packages/vscode-extension/out/package + run: | + VSIX_FILE=$(ls *.vsix 2>/dev/null | head -1) + if [ -z "$VSIX_FILE" ]; then + echo "::error::No VSIX file found in out/package/" + exit 1 + fi + echo "file=$VSIX_FILE" >> $GITHUB_OUTPUT + echo "path=packages/vscode-extension/out/package/$VSIX_FILE" >> $GITHUB_OUTPUT + echo "Found VSIX: $VSIX_FILE" + + - name: Publish to VS Marketplace + working-directory: packages/vscode-extension/out/package + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + npx @vscode/vsce publish --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + name: VS Code Extension v${{ steps.version.outputs.version }} + body_path: packages/vscode-extension/CHANGELOG.md + generate_release_notes: true + files: | + ${{ steps.vsix.outputs.path }} diff --git a/.github/workflows/vscode-ci.yml b/.github/workflows/vscode-ci.yml new file mode 100644 index 00000000..5d97ba30 --- /dev/null +++ b/.github/workflows/vscode-ci.yml @@ -0,0 +1,164 @@ +# VS Code Extension CI +# Migrated from Azure DevOps pipeline + +name: VS Code Extension CI + +on: + pull_request: + branches: [main] + paths: + - 'packages/vscode-extension/**' + - '.github/workflows/vscode-ci.yml' + push: + branches: [main] + paths: + - 'packages/vscode-extension/**' + - '.github/workflows/vscode-ci.yml' + +env: + NODE_VERSION: '20.x' + +jobs: + # Build extension for multiple channels + build: + name: Build (${{ matrix.channel }}) + runs-on: ubuntu-latest + strategy: + matrix: + channel: [preview, stable] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: packages/vscode-extension/package-lock.json + + - name: Setup PowerShell modules + shell: pwsh + run: | + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted + Install-Module -Name InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force + + - name: Install dependencies + working-directory: packages/vscode-extension + run: npm ci + + - name: Build extension + shell: pwsh + working-directory: packages/vscode-extension + run: | + Invoke-Build Build -Channel '${{ matrix.channel }}' + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: extension-${{ matrix.channel }} + path: packages/vscode-extension/out/package/ + retention-days: 7 + + # Cross-platform testing + test: + name: Test (${{ matrix.os }}, PowerShell ${{ matrix.pwsh && '7.x' || '5.1' }}) + needs: build + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + pwsh: [true] + include: + # Add PowerShell 5.1 test on Windows + - os: windows-latest + pwsh: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + cache-dependency-path: packages/vscode-extension/package-lock.json + + - name: Download extension artifact + uses: actions/download-artifact@v4 + with: + name: extension-preview + path: packages/vscode-extension/out/package + + - name: Install dependencies + working-directory: packages/vscode-extension + run: npm ci + + - name: Compile extension and tests + working-directory: packages/vscode-extension + run: npm run compile + + - name: Run tests (Linux) + if: runner.os == 'Linux' + working-directory: packages/vscode-extension + run: xvfb-run -a npm test + + - name: Run tests (macOS/Windows - PowerShell 7.x) + if: runner.os != 'Linux' && matrix.pwsh == true + shell: pwsh + working-directory: packages/vscode-extension + run: npm test + + - name: Run tests (Windows - PowerShell 5.1) + if: matrix.pwsh == false + shell: powershell + working-directory: packages/vscode-extension + run: npm test + + # Auto-publish preview on main merge + publish-preview: + name: Publish Preview + needs: [build, test] + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + runs-on: ubuntu-latest + environment: release + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + + - name: Download preview artifact + uses: actions/download-artifact@v4 + with: + name: extension-preview + path: packages/vscode-extension/out/package + + - name: Install vsce + run: npm install -g @vscode/vsce + + - name: Find VSIX file + id: vsix + working-directory: packages/vscode-extension/out/package + run: | + VSIX_FILE=$(ls *.vsix 2>/dev/null | head -1) + if [ -z "$VSIX_FILE" ]; then + echo "::error::No VSIX file found in out/package/" + exit 1 + fi + echo "file=$VSIX_FILE" >> $GITHUB_OUTPUT + echo "Found VSIX: $VSIX_FILE" + + - name: Publish to VS Marketplace (Pre-release) + working-directory: packages/vscode-extension/out/package + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: | + vsce publish --pre-release --packagePath "${{ steps.vsix.outputs.file }}" --pat "$VSCE_PAT" diff --git a/.gitignore b/.gitignore index c82c1657..a1492360 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,11 @@ site/ *.user .sass-cache .jekyll-metadata + +# Monorepo build artifacts +packages/*/out/ +packages/*/node_modules/ +packages/*/*.vsix + +# Build artifacts +*.nupkg diff --git a/MONOREPO_MIGRATION.md b/MONOREPO_MIGRATION.md new file mode 100644 index 00000000..73c3ed95 --- /dev/null +++ b/MONOREPO_MIGRATION.md @@ -0,0 +1,154 @@ +# Monorepo Migration Guide + +## Overview + +This repository has been restructured to serve as a monorepo for the complete PSDocs ecosystem, consolidating: +- **PSDocs** (core engine from microsoft/PSDocs) +- **PSDocs.Azure** (existing content, now in packages/psdocs-azure/) +- **VS Code Extension** (from microsoft/PSDocs-vscode) + +## Repository Structure + +``` +packages/ +├── psdocs/ # PSDocs engine (to be added via git subtree) +├── psdocs-azure/ # Azure IaC documentation generator (existing content) +└── vscode-extension/ # VS Code extension (to be added via git subtree) + +docs/ +├── psdocs/ # PSDocs engine docs (to be added via git subtree) +├── psdocs-azure/ # PSDocs.Azure docs (existing content) +└── vscode/ # VS Code extension docs + +build/ +└── common.ps1 # Shared build utilities + +.github/workflows/ +├── ci.yml # Main CI workflow with path-based filtering +├── vscode-ci.yml # VS Code extension CI (build, test, preview publish) +├── codeql.yml # CodeQL security scanning +├── release-psdocs.yml # Release workflow for PSDocs engine +├── release-psdocs-azure.yml # Release workflow for PSDocs.Azure +└── release-vscode.yml # Release workflow for VS Code extension (stable) +``` + +## Building the Monorepo + +### Build all packages + +```powershell +./build.ps1 -Build +``` + +### Build specific package + +```powershell +# Build PSDocs engine only +./build.ps1 -Package psdocs -Build + +# Build PSDocs.Azure (also builds PSDocs if needed) +./build.ps1 -Package psdocs-azure -Build -Test + +# Build VS Code extension +./build.ps1 -Package vscode -Build +``` + +### Clean build artifacts + +```powershell +./build.ps1 -Clean +``` + +## CI/CD Workflows + +### CI Workflow + +The CI workflow (`.github/workflows/ci.yml`) uses path-based filtering to only build packages that have changed: + +- Changes to `packages/psdocs/**` trigger the PSDocs build +- Changes to `packages/psdocs-azure/**` trigger the PSDocs.Azure build (which depends on PSDocs) +- Changes to `packages/vscode-extension/**` trigger the VS Code extension build + +### Release Workflows + +Each component has its own release workflow triggered by version-tagged commits: + +- **PSDocs**: Tag format `psdocs-v{version}` (e.g., `psdocs-v0.10.0`) +- **PSDocs.Azure**: Tag format `psdocs-azure-v{version}` (e.g., `psdocs-azure-v0.4.0`) +- **VS Code Extension**: Tag format `vscode-v{version}` (e.g., `vscode-v1.1.0`) + +## Versioning Strategy + +Each component is versioned independently: + +- Each package maintains its own CHANGELOG.md +- Version tags are prefixed to identify the component +- Releases are published separately to their respective platforms: + - PSDocs and PSDocs.Azure → PowerShell Gallery + - VS Code Extension → Visual Studio Marketplace + +## Development Workflow + +### Working on PSDocs.Azure + +1. Make changes in `packages/psdocs-azure/` +2. Test locally: `./build.ps1 -Package psdocs-azure -Build -Test` +3. Commit and push - CI will automatically build and test +4. Tag for release: `git tag psdocs-azure-v{version}` + +### Working on PSDocs Engine + +1. Make changes in `packages/psdocs/` +2. Test locally: `./build.ps1 -Package psdocs -Build -Test` +3. Test PSDocs.Azure still works: `./build.ps1 -Package psdocs-azure -Test` +4. Tag for release: `git tag psdocs-v{version}` + +### Working on VS Code Extension + +1. Make changes in `packages/vscode-extension/` +2. Test locally: `./build.ps1 -Package vscode -Build` +3. Tag for release: `git tag vscode-v{version}` + +## Migration Notes + +### Path Updates + +All documentation and template paths have been updated in the main README: +- `docs/` → `docs/psdocs-azure/` +- `templates/` → `packages/psdocs-azure/templates/` +- `examples/` → `packages/psdocs-azure/examples/` + +### Build Artifacts + +Build artifacts are now generated in: +- `packages/psdocs/out/` - PSDocs engine +- `packages/psdocs-azure/out/` - PSDocs.Azure module +- `packages/vscode-extension/*.vsix` - VS Code extension package + +### Gitignore Updates + +The `.gitignore` has been updated to exclude: +- `packages/*/out/` - Build artifacts from all packages +- `packages/*/node_modules/` - Node.js dependencies +- `packages/*/*.vsix` - VS Code extension packages +- `*.nupkg` - NuGet packages + +## Future Subtree Updates + +To pull updates from the source repositories later: + +```bash +# Update PSDocs engine +git subtree pull --prefix=packages/psdocs https://github.com/microsoft/PSDocs.git main --squash + +# Update VS Code extension +git subtree pull --prefix=packages/vscode-extension https://github.com/microsoft/PSDocs-vscode.git main --squash +``` + +## Questions? + +For questions about: +- PSDocs engine: See `packages/psdocs/README.md` (after subtree merge) +- PSDocs.Azure: See `packages/psdocs-azure/src/PSDocs.Azure/README.md` +- VS Code extension: See `packages/vscode-extension/README.md` (after subtree merge) +- Monorepo structure: File an issue on this repository diff --git a/README.md b/README.md index b1a9d620..f8d2a3d8 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,43 @@ Generate markdown from Azure infrastructure as code (IaC) artifacts. ![ci-badge] +## Repository Structure + +This is a monorepo containing the complete PSDocs ecosystem: + +| Package | Description | Location | +|---------|-------------|----------| +| **PSDocs** | Core documentation engine | [`packages/psdocs/`](./packages/psdocs/) | +| **PSDocs.Azure** | Azure IaC documentation generator | [`packages/psdocs-azure/`](./packages/psdocs-azure/) | +| **VS Code Extension** | PSDocs extension for VS Code | [`packages/vscode-extension/`](./packages/vscode-extension/) | + +### Building + +```powershell +# Build all packages +./build.ps1 -Build + +# Build and test specific package +./build.ps1 -Package psdocs-azure -Build -Test + +# Build VS Code extension +./build.ps1 -Package vscode -Build +``` + +### Versioning + +Each component is versioned independently using prefixed tags: +- PSDocs: `psdocs-v{version}` (e.g., `psdocs-v0.10.0`) +- PSDocs.Azure: `psdocs-azure-v{version}` (e.g., `psdocs-azure-v0.4.0`) +- VS Code Extension: `vscode-v{version}` (e.g., `vscode-v1.1.0`) + +## Features + Features of PSDocs for Azure include: -- [Ready to go](docs/overview.md#ready-to-go) - Use pre-built templates. -- [DevOps](docs/overview.md#devops) - Generate within a continuous integration (CI) pipeline. -- [Cross-platform](docs/overview.md#cross-platform) - Run on MacOS, Linux, and Windows. +- [Ready to go](docs/psdocs-azure/overview.md#ready-to-go) - Use pre-built templates. +- [DevOps](docs/psdocs-azure/overview.md#devops) - Generate within a continuous integration (CI) pipeline. +- [Cross-platform](docs/psdocs-azure/overview.md#cross-platform) - Run on MacOS, Linux, and Windows. ## Support @@ -282,20 +314,20 @@ PSDocs for Azure extends PowerShell with the following cmdlets and concepts. The following commands exist in the `PSDocs.Azure` module: -- [Get-AzDocTemplateFile](docs/commands/en-US/Get-AzDocTemplateFile.md) - Get Azure template files within a directory structure. +- [Get-AzDocTemplateFile](docs/psdocs-azure/commands/en-US/Get-AzDocTemplateFile.md) - Get Azure template files within a directory structure. ### Concepts The following conceptual topics exist in the `PSDocs.Azure` module: -- [Badges](docs/concepts/en-US/about_PSDocs_Azure_Badges.md) -- [Configuration](docs/concepts/en-US/about_PSDocs_Azure_Configuration.md) - - [AZURE_SNIPPET_SKIP_DEFAULT_VALUE_FN](docs/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_snippet_skip_default_value_fn) - - [AZURE_SNIPPET_SKIP_OPTIONAL_PARAMETER](docs/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_snippet_skip_optional_parameter) - - [AZURE_USE_PARAMETER_FILE_SNIPPET](docs/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_use_parameter_file_snippet) - - [AZURE_USE_COMMAND_LINE_SNIPPET](docs/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_use_command_line_snippet) -- [Conventions](docs/concepts/en-US/about_PSDocs_Azure_Conventions.md) - - [Azure.NameByParentPath](docs/concepts/en-US/about_PSDocs_Azure_Conventions.md#azurenamebyparentpath) +- [Badges](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Badges.md) +- [Configuration](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md) + - [AZURE_SNIPPET_SKIP_DEFAULT_VALUE_FN](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_snippet_skip_default_value_fn) + - [AZURE_SNIPPET_SKIP_OPTIONAL_PARAMETER](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_snippet_skip_optional_parameter) + - [AZURE_USE_PARAMETER_FILE_SNIPPET](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_use_parameter_file_snippet) + - [AZURE_USE_COMMAND_LINE_SNIPPET](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md#azure_use_command_line_snippet) +- [Conventions](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Conventions.md) + - [Azure.NameByParentPath](docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Conventions.md#azurenamebyparentpath) ## Changes and versioning @@ -329,11 +361,11 @@ This project is [licensed under the MIT License](LICENSE). [issue]: https://github.com/Azure/PSDocs.Azure/issues [discussion]: https://github.com/Azure/PSDocs.Azure/discussions -[install]: docs/install-instructions.md +[install]: docs/psdocs-azure/install-instructions.md [ci-badge]: https://dev.azure.com/PSDocs/PSDocs.Azure/_apis/build/status/PSDocs.Azure-CI?branchName=main [module]: https://www.powershellgallery.com/packages/PSDocs.Azure [engine]: https://github.com/microsoft/PSDocs [create-workflow]: https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file -[source-template]: templates/storage/v1/template.json -[output-template]: templates/storage/v1/README.md -[FAQ]: docs/overview.md#frequently-asked-questions-faq +[source-template]: packages/psdocs-azure/templates/storage/v1/template.json +[output-template]: packages/psdocs-azure/templates/storage/v1/README.md +[FAQ]: docs/psdocs-azure/overview.md#frequently-asked-questions-faq diff --git a/build.ps1 b/build.ps1 index 4abe1e30..b56332a7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,10 +1,105 @@ +#!/usr/bin/env pwsh # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -# Note: -# This manually builds the project locally +# Root build orchestrator for PSDocs monorepo -. ./scripts/pipeline-deps.ps1 -Invoke-Build Test -AssertStyle Client +[CmdletBinding()] +param( + [Parameter()] + [ValidateSet('psdocs', 'psdocs-azure', 'vscode', 'all')] + [string]$Package = 'all', + + [Parameter()] + [switch]$Build, + + [Parameter()] + [switch]$Test, + + [Parameter()] + [switch]$Clean +) -Write-Host 'If no build errors occurred. The module has been saved to out/modules/PSDocs.Azure' +$ErrorActionPreference = 'Stop' + +# Import common utilities +. $PSScriptRoot/build/common.ps1 + +function Invoke-PackageBuild { + param( + [string]$PackagePath, + [string]$PackageName, + [switch]$Build, + [switch]$Test, + [switch]$Clean + ) + + if (-not (Test-Path $PackagePath)) { + Write-Host "Package '$PackageName' not found at $PackagePath - skipping" -ForegroundColor Yellow + return + } + + Write-Host "`n========================================" -ForegroundColor Cyan + Write-Host "Building: $PackageName" -ForegroundColor Cyan + Write-Host "========================================`n" -ForegroundColor Cyan + + Push-Location $PackagePath + try { + $buildScript = './pipeline.build.ps1' + if (Test-Path $buildScript) { + # pipeline.build.ps1 is an InvokeBuild script - use Invoke-Build + $tasks = @() + if ($Clean) { $tasks += 'Clean' } + if ($Build) { $tasks += 'Build' } + if ($Test) { $tasks += 'Test' } + if ($tasks.Count -gt 0) { + Invoke-Build -Task $tasks -File $buildScript + } + } elseif (Test-Path 'package.json') { + # Node.js package (VS Code extension) + if ($Clean) { + Remove-Item -Path 'node_modules', 'out', '*.vsix' -Recurse -Force -ErrorAction SilentlyContinue + } + if ($Build) { + npm ci + npm run compile + } + if ($Test) { + npm run lint + } + } + } + finally { + Pop-Location + } +} + +# Install dependencies +Install-BuildDependencies + +# Determine which packages to build +$packages = @() +switch ($Package) { + 'psdocs' { $packages = @('psdocs') } + 'psdocs-azure' { $packages = @('psdocs', 'psdocs-azure') } # psdocs-azure depends on psdocs + 'vscode' { $packages = @('vscode') } + 'all' { $packages = @('psdocs', 'psdocs-azure', 'vscode') } +} + +# Build packages in order +foreach ($pkg in $packages) { + $pkgPath = switch ($pkg) { + 'psdocs' { "$PSScriptRoot/packages/psdocs" } + 'psdocs-azure' { "$PSScriptRoot/packages/psdocs-azure" } + 'vscode' { "$PSScriptRoot/packages/vscode-extension" } + } + + Invoke-PackageBuild -PackagePath $pkgPath -PackageName $pkg -Build:$Build -Test:$Test -Clean:$Clean + + # After building psdocs, set up module path for psdocs-azure + if ($pkg -eq 'psdocs' -and $Build) { + Get-LocalPSDocsModule | Out-Null + } +} + +Write-Host "`n✅ Build complete!" -ForegroundColor Green diff --git a/build/common.ps1 b/build/common.ps1 new file mode 100644 index 00000000..55bea25d --- /dev/null +++ b/build/common.ps1 @@ -0,0 +1,86 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Common build utilities for all packages in the monorepo +[CmdletBinding()] +param( + [Parameter()] + [string]$Component +) + +function Get-MonorepoRoot { + $current = $PSScriptRoot + while ($current -ne [System.IO.Path]::GetPathRoot($current)) { + if (Test-Path (Join-Path $current 'packages')) { + return $current + } + $current = Split-Path $current -Parent + } + throw "Could not find monorepo root" +} + +function Install-BuildDependencies { + [CmdletBinding()] + param() + + Write-Host "Checking build dependencies..." -ForegroundColor Cyan + + # Try to install NuGet provider + try { + if ($Null -eq (Get-PackageProvider -Name NuGet -ErrorAction Ignore)) { + Write-Host " Installing NuGet package provider..." + Install-PackageProvider -Name NuGet -Force -Scope CurrentUser -ErrorAction Stop | Out-Null + } + } catch { + Write-Host " Note: NuGet provider installation skipped (not available in this environment)" -ForegroundColor Yellow + } + + # Try to install/update PowerShellGet + try { + if ($Null -eq (Get-InstalledModule -Name PowerShellGet -MinimumVersion 2.2.1 -ErrorAction Ignore -WarningAction SilentlyContinue)) { + Write-Host " Installing PowerShellGet module..." + Install-Module PowerShellGet -MinimumVersion 2.2.1 -Scope CurrentUser -Force -AllowClobber -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null + } + } catch { + Write-Host " Note: PowerShellGet installation skipped (not available in this environment)" -ForegroundColor Yellow + } + + # Try to install InvokeBuild + try { + if ($Null -eq (Get-InstalledModule -Name InvokeBuild -MinimumVersion 5.4.0 -ErrorAction Ignore)) { + Write-Host " Installing InvokeBuild module..." + Install-Module InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force -ErrorAction Stop | Out-Null + } + } catch { + Write-Host " Note: InvokeBuild installation skipped (not available in this environment)" -ForegroundColor Yellow + } + + # Try to install Pester + try { + if ($Null -eq (Get-InstalledModule -Name Pester -MinimumVersion 5.0.0 -ErrorAction Ignore)) { + Write-Host " Installing Pester module..." + Install-Module Pester -MinimumVersion 5.0.0 -Scope CurrentUser -Force -SkipPublisherCheck -ErrorAction Stop | Out-Null + } + } catch { + Write-Host " Note: Pester installation skipped (not available in this environment)" -ForegroundColor Yellow + } + + Write-Host " Dependencies check complete!" -ForegroundColor Green +} + +function Get-LocalPSDocsModule { + [CmdletBinding()] + param() + + $root = Get-MonorepoRoot + $localModule = Join-Path $root 'packages/psdocs/out/modules/PSDocs' + + if (Test-Path $localModule) { + Write-Host "Using local PSDocs module from: $localModule" + $env:PSModulePath = "$localModule;$env:PSModulePath" + return $true + } + + Write-Host "Local PSDocs module not found, will use PSGallery version" + return $false +} diff --git a/docs/assets/ms_icon.png b/docs/psdocs-azure/assets/ms_icon.png similarity index 100% rename from docs/assets/ms_icon.png rename to docs/psdocs-azure/assets/ms_icon.png diff --git a/docs/assets/stylesheets/extra.css b/docs/psdocs-azure/assets/stylesheets/extra.css similarity index 100% rename from docs/assets/stylesheets/extra.css rename to docs/psdocs-azure/assets/stylesheets/extra.css diff --git a/docs/commands/en-US/Get-AzDocTemplateFile.md b/docs/psdocs-azure/commands/en-US/Get-AzDocTemplateFile.md similarity index 100% rename from docs/commands/en-US/Get-AzDocTemplateFile.md rename to docs/psdocs-azure/commands/en-US/Get-AzDocTemplateFile.md diff --git a/docs/commands/en-US/PSDocs.Azure.md b/docs/psdocs-azure/commands/en-US/PSDocs.Azure.md similarity index 100% rename from docs/commands/en-US/PSDocs.Azure.md rename to docs/psdocs-azure/commands/en-US/PSDocs.Azure.md diff --git a/docs/concepts/en-US/about_PSDocs_Azure_Badges.md b/docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Badges.md similarity index 100% rename from docs/concepts/en-US/about_PSDocs_Azure_Badges.md rename to docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Badges.md diff --git a/docs/concepts/en-US/about_PSDocs_Azure_Configuration.md b/docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md similarity index 100% rename from docs/concepts/en-US/about_PSDocs_Azure_Configuration.md rename to docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Configuration.md diff --git a/docs/concepts/en-US/about_PSDocs_Azure_Conventions.md b/docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Conventions.md similarity index 100% rename from docs/concepts/en-US/about_PSDocs_Azure_Conventions.md rename to docs/psdocs-azure/concepts/en-US/about_PSDocs_Azure_Conventions.md diff --git a/docs/creating-your-pipeline.md b/docs/psdocs-azure/creating-your-pipeline.md similarity index 100% rename from docs/creating-your-pipeline.md rename to docs/psdocs-azure/creating-your-pipeline.md diff --git a/docs/favicon.ico b/docs/psdocs-azure/favicon.ico similarity index 100% rename from docs/favicon.ico rename to docs/psdocs-azure/favicon.ico diff --git a/docs/hooks.py b/docs/psdocs-azure/hooks.py similarity index 100% rename from docs/hooks.py rename to docs/psdocs-azure/hooks.py diff --git a/docs/index.md b/docs/psdocs-azure/index.md similarity index 100% rename from docs/index.md rename to docs/psdocs-azure/index.md diff --git a/docs/install-instructions.md b/docs/psdocs-azure/install-instructions.md similarity index 100% rename from docs/install-instructions.md rename to docs/psdocs-azure/install-instructions.md diff --git a/docs/license-contributing.md b/docs/psdocs-azure/license-contributing.md similarity index 100% rename from docs/license-contributing.md rename to docs/psdocs-azure/license-contributing.md diff --git a/docs/overview.md b/docs/psdocs-azure/overview.md similarity index 100% rename from docs/overview.md rename to docs/psdocs-azure/overview.md diff --git a/docs/publish/azure-webapp.md b/docs/psdocs-azure/publish/azure-webapp.md similarity index 100% rename from docs/publish/azure-webapp.md rename to docs/psdocs-azure/publish/azure-webapp.md diff --git a/docs/publish/blob-storage.md b/docs/psdocs-azure/publish/blob-storage.md similarity index 100% rename from docs/publish/blob-storage.md rename to docs/psdocs-azure/publish/blob-storage.md diff --git a/docs/publish/devops-wiki.md b/docs/psdocs-azure/publish/devops-wiki.md similarity index 100% rename from docs/publish/devops-wiki.md rename to docs/psdocs-azure/publish/devops-wiki.md diff --git a/docs/release.md b/docs/psdocs-azure/release.md similarity index 100% rename from docs/release.md rename to docs/psdocs-azure/release.md diff --git a/docs/setup/configuring-options.md b/docs/psdocs-azure/setup/configuring-options.md similarity index 100% rename from docs/setup/configuring-options.md rename to docs/psdocs-azure/setup/configuring-options.md diff --git a/docs/setup/configuring-snippets.md b/docs/psdocs-azure/setup/configuring-snippets.md similarity index 100% rename from docs/setup/configuring-snippets.md rename to docs/psdocs-azure/setup/configuring-snippets.md diff --git a/docs/support.md b/docs/psdocs-azure/support.md similarity index 100% rename from docs/support.md rename to docs/psdocs-azure/support.md diff --git a/docs/templates/index.md b/docs/psdocs-azure/templates/index.md similarity index 100% rename from docs/templates/index.md rename to docs/psdocs-azure/templates/index.md diff --git a/docs/troubleshooting.md b/docs/psdocs-azure/troubleshooting.md similarity index 100% rename from docs/troubleshooting.md rename to docs/psdocs-azure/troubleshooting.md diff --git a/docs/using-metadata.md b/docs/psdocs-azure/using-metadata.md similarity index 100% rename from docs/using-metadata.md rename to docs/psdocs-azure/using-metadata.md diff --git a/docs/psdocs/.gitkeep b/docs/psdocs/.gitkeep new file mode 100644 index 00000000..d5179f27 --- /dev/null +++ b/docs/psdocs/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder for PSDocs documentation +# Content will be added via git subtree from microsoft/PSDocs diff --git a/docs/vscode/.gitkeep b/docs/vscode/.gitkeep new file mode 100644 index 00000000..a2df424f --- /dev/null +++ b/docs/vscode/.gitkeep @@ -0,0 +1,2 @@ +# Placeholder for VS Code extension documentation +# Content will be added via git subtree from microsoft/PSDocs-vscode diff --git a/PSDocs.Azure.sln b/packages/psdocs-azure/PSDocs.Azure.sln similarity index 100% rename from PSDocs.Azure.sln rename to packages/psdocs-azure/PSDocs.Azure.sln diff --git a/examples/bicep/storage/v1/README.md b/packages/psdocs-azure/examples/bicep/storage/v1/README.md similarity index 100% rename from examples/bicep/storage/v1/README.md rename to packages/psdocs-azure/examples/bicep/storage/v1/README.md diff --git a/examples/bicep/storage/v1/main.bicep b/packages/psdocs-azure/examples/bicep/storage/v1/main.bicep similarity index 100% rename from examples/bicep/storage/v1/main.bicep rename to packages/psdocs-azure/examples/bicep/storage/v1/main.bicep diff --git a/examples/bicep/storage/v1/main.json b/packages/psdocs-azure/examples/bicep/storage/v1/main.json similarity index 100% rename from examples/bicep/storage/v1/main.json rename to packages/psdocs-azure/examples/bicep/storage/v1/main.json diff --git a/modules.json b/packages/psdocs-azure/modules.json similarity index 100% rename from modules.json rename to packages/psdocs-azure/modules.json diff --git a/pipeline.build.ps1 b/packages/psdocs-azure/pipeline.build.ps1 similarity index 97% rename from pipeline.build.ps1 rename to packages/psdocs-azure/pipeline.build.ps1 index 4aec7955..556f15d4 100644 --- a/pipeline.build.ps1 +++ b/packages/psdocs-azure/pipeline.build.ps1 @@ -197,11 +197,11 @@ task ModuleDependencies NuGet, PSDocs task CopyModule { CopyModuleFiles -Path src/PSDocs.Azure -DestinationPath out/modules/PSDocs.Azure; - # Copy LICENSE - Copy-Item -Path LICENSE -Destination out/modules/PSDocs.Azure; + # Copy LICENSE from repo root + Copy-Item -Path ../../LICENSE -Destination out/modules/PSDocs.Azure; - # Copy third party notices - Copy-Item -Path ThirdPartyNotices.txt -Destination out/modules/PSDocs.Azure; + # Copy third party notices from repo root + Copy-Item -Path ../../ThirdPartyNotices.txt -Destination out/modules/PSDocs.Azure; } task BuildDotNet { @@ -234,7 +234,7 @@ task BuildHelp BuildModule, PlatyPS, { &$pwshPath -Command { # Generate MAML and about topics Import-Module -Name PlatyPS -Verbose:$False; - $Null = New-ExternalHelp -OutputPath 'out/docs/PSDocs.Azure' -Path '.\docs\commands\en-US', '.\docs\concepts\en-US' -Force; + $Null = New-ExternalHelp -OutputPath 'out/docs/PSDocs.Azure' -Path '../../docs/psdocs-azure/commands/en-US', '../../docs/psdocs-azure/concepts/en-US' -Force; } } diff --git a/ps-docs.yaml b/packages/psdocs-azure/ps-docs.yaml similarity index 100% rename from ps-docs.yaml rename to packages/psdocs-azure/ps-docs.yaml diff --git a/ps-project.yaml b/packages/psdocs-azure/ps-project.yaml similarity index 100% rename from ps-project.yaml rename to packages/psdocs-azure/ps-project.yaml diff --git a/ps-rule.yaml b/packages/psdocs-azure/ps-rule.yaml similarity index 100% rename from ps-rule.yaml rename to packages/psdocs-azure/ps-rule.yaml diff --git a/src/PSDocs.Azure/Configuration/OutputEncoding.cs b/packages/psdocs-azure/src/PSDocs.Azure/Configuration/OutputEncoding.cs similarity index 100% rename from src/PSDocs.Azure/Configuration/OutputEncoding.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Configuration/OutputEncoding.cs diff --git a/src/PSDocs.Azure/Configuration/OutputOption.cs b/packages/psdocs-azure/src/PSDocs.Azure/Configuration/OutputOption.cs similarity index 100% rename from src/PSDocs.Azure/Configuration/OutputOption.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Configuration/OutputOption.cs diff --git a/src/PSDocs.Azure/Configuration/PSDocumentOption.cs b/packages/psdocs-azure/src/PSDocs.Azure/Configuration/PSDocumentOption.cs similarity index 100% rename from src/PSDocs.Azure/Configuration/PSDocumentOption.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Configuration/PSDocumentOption.cs diff --git a/src/PSDocs.Azure/Data/Metadata/ITemplateLink.cs b/packages/psdocs-azure/src/PSDocs.Azure/Data/Metadata/ITemplateLink.cs similarity index 100% rename from src/PSDocs.Azure/Data/Metadata/ITemplateLink.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Data/Metadata/ITemplateLink.cs diff --git a/src/PSDocs.Azure/Data/Metadata/TemplateLink.cs b/packages/psdocs-azure/src/PSDocs.Azure/Data/Metadata/TemplateLink.cs similarity index 100% rename from src/PSDocs.Azure/Data/Metadata/TemplateLink.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Data/Metadata/TemplateLink.cs diff --git a/src/PSDocs.Azure/PSDocs.Azure.csproj b/packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.csproj similarity index 100% rename from src/PSDocs.Azure/PSDocs.Azure.csproj rename to packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.csproj diff --git a/src/PSDocs.Azure/PSDocs.Azure.psd1 b/packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1 similarity index 100% rename from src/PSDocs.Azure/PSDocs.Azure.psd1 rename to packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psd1 diff --git a/src/PSDocs.Azure/PSDocs.Azure.psm1 b/packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psm1 similarity index 100% rename from src/PSDocs.Azure/PSDocs.Azure.psm1 rename to packages/psdocs-azure/src/PSDocs.Azure/PSDocs.Azure.psm1 diff --git a/src/PSDocs.Azure/Pipeline/Exceptions.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/Exceptions.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/Exceptions.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/Exceptions.cs diff --git a/src/PSDocs.Azure/Pipeline/LoggingExtensions.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/LoggingExtensions.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/LoggingExtensions.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/LoggingExtensions.cs diff --git a/src/PSDocs.Azure/Pipeline/Output/PSPipelineWriter.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/Output/PSPipelineWriter.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/Output/PSPipelineWriter.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/Output/PSPipelineWriter.cs diff --git a/src/PSDocs.Azure/Pipeline/PathBuilder.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PathBuilder.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/PathBuilder.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PathBuilder.cs diff --git a/src/PSDocs.Azure/Pipeline/PipelineBuilder.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineBuilder.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/PipelineBuilder.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineBuilder.cs diff --git a/src/PSDocs.Azure/Pipeline/PipelineContext.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineContext.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/PipelineContext.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineContext.cs diff --git a/src/PSDocs.Azure/Pipeline/PipelineWriter.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineWriter.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/PipelineWriter.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/PipelineWriter.cs diff --git a/src/PSDocs.Azure/Pipeline/TemplatePipeline.cs b/packages/psdocs-azure/src/PSDocs.Azure/Pipeline/TemplatePipeline.cs similarity index 100% rename from src/PSDocs.Azure/Pipeline/TemplatePipeline.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Pipeline/TemplatePipeline.cs diff --git a/src/PSDocs.Azure/Properties/AssemblyInfo.cs b/packages/psdocs-azure/src/PSDocs.Azure/Properties/AssemblyInfo.cs similarity index 100% rename from src/PSDocs.Azure/Properties/AssemblyInfo.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Properties/AssemblyInfo.cs diff --git a/src/PSDocs.Azure/README.md b/packages/psdocs-azure/src/PSDocs.Azure/README.md similarity index 100% rename from src/PSDocs.Azure/README.md rename to packages/psdocs-azure/src/PSDocs.Azure/README.md diff --git a/src/PSDocs.Azure/Resources/Diagnostics.Designer.cs b/packages/psdocs-azure/src/PSDocs.Azure/Resources/Diagnostics.Designer.cs similarity index 100% rename from src/PSDocs.Azure/Resources/Diagnostics.Designer.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Resources/Diagnostics.Designer.cs diff --git a/src/PSDocs.Azure/Resources/Diagnostics.resx b/packages/psdocs-azure/src/PSDocs.Azure/Resources/Diagnostics.resx similarity index 100% rename from src/PSDocs.Azure/Resources/Diagnostics.resx rename to packages/psdocs-azure/src/PSDocs.Azure/Resources/Diagnostics.resx diff --git a/src/PSDocs.Azure/Resources/PSDocsResources.Designer.cs b/packages/psdocs-azure/src/PSDocs.Azure/Resources/PSDocsResources.Designer.cs similarity index 100% rename from src/PSDocs.Azure/Resources/PSDocsResources.Designer.cs rename to packages/psdocs-azure/src/PSDocs.Azure/Resources/PSDocsResources.Designer.cs diff --git a/src/PSDocs.Azure/Resources/PSDocsResources.resx b/packages/psdocs-azure/src/PSDocs.Azure/Resources/PSDocsResources.resx similarity index 100% rename from src/PSDocs.Azure/Resources/PSDocsResources.resx rename to packages/psdocs-azure/src/PSDocs.Azure/Resources/PSDocsResources.resx diff --git a/src/PSDocs.Azure/docs/Azure.Conventions.Doc.ps1 b/packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Conventions.Doc.ps1 similarity index 100% rename from src/PSDocs.Azure/docs/Azure.Conventions.Doc.ps1 rename to packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Conventions.Doc.ps1 diff --git a/src/PSDocs.Azure/docs/Azure.Selector.Doc.yaml b/packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Selector.Doc.yaml similarity index 100% rename from src/PSDocs.Azure/docs/Azure.Selector.Doc.yaml rename to packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Selector.Doc.yaml diff --git a/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 b/packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 similarity index 100% rename from src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 rename to packages/psdocs-azure/src/PSDocs.Azure/docs/Azure.Template.Doc.ps1 diff --git a/src/PSDocs.Azure/en/PSDocs-strings.psd1 b/packages/psdocs-azure/src/PSDocs.Azure/en/PSDocs-strings.psd1 similarity index 100% rename from src/PSDocs.Azure/en/PSDocs-strings.psd1 rename to packages/psdocs-azure/src/PSDocs.Azure/en/PSDocs-strings.psd1 diff --git a/templates/acr/v1/README.md b/packages/psdocs-azure/templates/acr/v1/README.md similarity index 100% rename from templates/acr/v1/README.md rename to packages/psdocs-azure/templates/acr/v1/README.md diff --git a/templates/acr/v1/template.json b/packages/psdocs-azure/templates/acr/v1/template.json similarity index 100% rename from templates/acr/v1/template.json rename to packages/psdocs-azure/templates/acr/v1/template.json diff --git a/templates/keyvault/v1/README.md b/packages/psdocs-azure/templates/keyvault/v1/README.md similarity index 100% rename from templates/keyvault/v1/README.md rename to packages/psdocs-azure/templates/keyvault/v1/README.md diff --git a/templates/keyvault/v1/template.json b/packages/psdocs-azure/templates/keyvault/v1/template.json similarity index 100% rename from templates/keyvault/v1/template.json rename to packages/psdocs-azure/templates/keyvault/v1/template.json diff --git a/templates/storage/v1/README.md b/packages/psdocs-azure/templates/storage/v1/README.md similarity index 100% rename from templates/storage/v1/README.md rename to packages/psdocs-azure/templates/storage/v1/README.md diff --git a/templates/storage/v1/template.json b/packages/psdocs-azure/templates/storage/v1/template.json similarity index 100% rename from templates/storage/v1/template.json rename to packages/psdocs-azure/templates/storage/v1/template.json diff --git a/tests/PSDocs.Azure.Tests/.ps-docs/azure-template-badges.md b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/.ps-docs/azure-template-badges.md similarity index 100% rename from tests/PSDocs.Azure.Tests/.ps-docs/azure-template-badges.md rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/.ps-docs/azure-template-badges.md diff --git a/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 similarity index 100% rename from tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Common.Tests.ps1 diff --git a/tests/PSDocs.Azure.Tests/Azure.Conventions.Tests.ps1 b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Conventions.Tests.ps1 similarity index 100% rename from tests/PSDocs.Azure.Tests/Azure.Conventions.Tests.ps1 rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Conventions.Tests.ps1 diff --git a/tests/PSDocs.Azure.Tests/Azure.Options.Tests.ps1 b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Options.Tests.ps1 similarity index 100% rename from tests/PSDocs.Azure.Tests/Azure.Options.Tests.ps1 rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Options.Tests.ps1 diff --git a/tests/PSDocs.Azure.Tests/Azure.QuickStart.Tests.ps1 b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.QuickStart.Tests.ps1 similarity index 100% rename from tests/PSDocs.Azure.Tests/Azure.QuickStart.Tests.ps1 rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.QuickStart.Tests.ps1 diff --git a/tests/PSDocs.Azure.Tests/Azure.Templates.Tests.ps1 b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Templates.Tests.ps1 similarity index 100% rename from tests/PSDocs.Azure.Tests/Azure.Templates.Tests.ps1 rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/Azure.Templates.Tests.ps1 diff --git a/tests/PSDocs.Azure.Tests/basic.template.json b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/basic.template.json similarity index 100% rename from tests/PSDocs.Azure.Tests/basic.template.json rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/basic.template.json diff --git a/tests/PSDocs.Azure.Tests/template-test/metadata.json b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/template-test/metadata.json similarity index 100% rename from tests/PSDocs.Azure.Tests/template-test/metadata.json rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/template-test/metadata.json diff --git a/tests/PSDocs.Azure.Tests/template-test/template.json b/packages/psdocs-azure/tests/PSDocs.Azure.Tests/template-test/template.json similarity index 100% rename from tests/PSDocs.Azure.Tests/template-test/template.json rename to packages/psdocs-azure/tests/PSDocs.Azure.Tests/template-test/template.json diff --git a/packages/psdocs/.editorconfig b/packages/psdocs/.editorconfig new file mode 100644 index 00000000..4dfc394f --- /dev/null +++ b/packages/psdocs/.editorconfig @@ -0,0 +1,83 @@ +# EditorConfig is awesome: https://EditorConfig.org + +# Top-most EditorConfig file +root = true + +# Default +[*] +charset = utf-8 +indent_style = space +insert_final_newline = true + +# Source code +[*.{cs,ps1,psd1,psm1}] +indent_size = 4 + +# Xml project files +[*.{csproj,resx,ps1xml}] +indent_size = 2 + +# C# files +[*.cs] + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_preferred_modifier_order.severity = suggestion +dotnet_sort_system_directives_first = true +dotnet_style_readonly_field = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion + +# License header +file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. + +# Suggest more modern language features when available +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion +dotnet_style_prefer_conditional_expression_over_return = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_switch_expression = false:none +csharp_style_prefer_pattern_matching = false:none + +# Prefer "var" everywhere +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_var_elsewhere = true:suggestion + +# Define the 'private_fields' symbol group: +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +# Define the 'private_static_fields' symbol group +dotnet_naming_symbols.private_static_fields.applicable_kinds = field +dotnet_naming_symbols.private_static_fields.applicable_accessibilities = private +dotnet_naming_symbols.private_static_fields.required_modifiers = static + +# Define the 'underscored' naming style +dotnet_naming_style.underscored.capitalization = pascal_case +dotnet_naming_style.underscored.required_prefix = _ + +# Private instance fields must use pascal case with a leading '_' +dotnet_naming_rule.private_fields_underscored.symbols = private_fields +dotnet_naming_rule.private_fields_underscored.style = underscored +dotnet_naming_rule.private_fields_underscored.severity = error + +# Exclude private static fields from underscored style +dotnet_naming_rule.private_static_fields_none.symbols = private_static_fields +dotnet_naming_rule.private_static_fields_none.style = underscored +dotnet_naming_rule.private_static_fields_none.severity = none diff --git a/packages/psdocs/.github/CODEOWNERS b/packages/psdocs/.github/CODEOWNERS new file mode 100644 index 00000000..306144a9 --- /dev/null +++ b/packages/psdocs/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# https://help.github.com/articles/about-codeowners/ +* @microsoft/psdocs diff --git a/packages/psdocs/.github/ISSUE_TEMPLATE/bug_report.md b/packages/psdocs/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..e66b0911 --- /dev/null +++ b/packages/psdocs/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Report errors or unexpected behaviour +--- + +**Description of the issue** + + + +**To Reproduce** + +Steps to reproduce the issue: + +```powershell + +``` + +**Expected behaviour** + + + +**Error output** + + + +```text + +``` + +**Module in use and version:** + +- Module: PSDocs +- Version: **[e.g. 0.9.0]** + +Captured output from `$PSVersionTable`: + +```text + +``` + +**Additional context** + + diff --git a/packages/psdocs/.github/ISSUE_TEMPLATE/feature_request.md b/packages/psdocs/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..3affb588 --- /dev/null +++ b/packages/psdocs/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea +--- + +**Is your feature request related to a problem? Please describe.** + + + +**Describe the solution you'd like** + + + +**Describe alternatives you've considered** + + + +**Additional context** + + diff --git a/packages/psdocs/.github/ISSUE_TEMPLATE/question.md b/packages/psdocs/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 00000000..496891c0 --- /dev/null +++ b/packages/psdocs/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,11 @@ +--- +name: Question +about: If you have a question, please check out Discussions +labels: 'question' +--- + +We use Issues as an issue tracker; for help, discussion, and support questions, please use Discussions. + +Thanks! 😁. + +- https://github.com/Microsoft/PSDocs/discussions diff --git a/packages/psdocs/.github/PULL_REQUEST_TEMPLATE.md b/packages/psdocs/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..36d68401 --- /dev/null +++ b/packages/psdocs/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,14 @@ +## PR Summary + + + +## PR Checklist + +- [ ] PR has a meaningful title +- [ ] Summarized changes +- [ ] Change is not breaking +- [ ] This PR is ready to merge and is not **Work in Progress** +- **Code changes** + - [ ] Have unit tests created/ updated + - [ ] Link to a filed issue + - [ ] [Change log](https://github.com/Microsoft/PSDocs/blob/main/CHANGELOG.md) has been updated with change under unreleased section diff --git a/packages/psdocs/.github/dependabot.yml b/packages/psdocs/.github/dependabot.yml new file mode 100644 index 00000000..6dae0475 --- /dev/null +++ b/packages/psdocs/.github/dependabot.yml @@ -0,0 +1,33 @@ +# +# Dependabot configuration +# + +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + +# Maintain dependencies for NuGet +- package-ecosystem: 'nuget' # See documentation for possible values + directory: '/' # Location of package manifests + schedule: + interval: 'daily' + labels: + - 'dependencies' + reviewers: + - 'microsoft/psdocs' + ignore: + # Ignore upgrades to PS 7.1 for tool chain components at this time + # Testing against PS 7.1 is already completed + - dependency-name: 'Microsoft.PowerShell.SDK' + +# Maintain dependencies for GitHub Actions +- package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: 'daily' + labels: + - 'ci-quality' + reviewers: + - 'microsoft/psdocs' diff --git a/packages/psdocs/.github/workflows/analyze.yaml b/packages/psdocs/.github/workflows/analyze.yaml new file mode 100644 index 00000000..e2bf9b39 --- /dev/null +++ b/packages/psdocs/.github/workflows/analyze.yaml @@ -0,0 +1,89 @@ +# +# Repository analysis +# + +# NOTES: +# This workflow uses PSRule, CodeQL, and DevSkim. +# You can read more about these linting tools and configuration options here: +# PSRule - https://aka.ms/ps-rule and https://github.com/Microsoft/PSRule.Rules.MSFT.OSS +# CodeQL - https://codeql.github.com/docs/codeql-overview/about-codeql/ +# DevSkim - https://github.com/microsoft/DevSkim-Action and https://github.com/Microsoft/DevSkim + +name: Analyze +on: + push: + branches: [main, 'release/*'] + pull_request: + branches: [main, 'release/*'] + schedule: + - cron: '24 22 * * 0' # At 10:24 PM, on Sunday each week + workflow_dispatch: + +permissions: {} + +jobs: + oss: + name: Analyze with PSRule + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run PSRule analysis + uses: microsoft/ps-rule@main + with: + modules: PSRule.Rules.MSFT.OSS + prerelease: true + outputFormat: Sarif + outputPath: reports/ps-rule-results.sarif + + - name: Upload results to security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: reports/ps-rule-results.sarif + + devskim: + name: Analyze with DevSkim + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run DevSkim scanner + uses: microsoft/DevSkim-Action@v1 + with: + directory-to-scan: . + + - name: Upload results to security tab + uses: github/codeql-action/upload-sarif@v2 + with: + sarif_file: devskim-results.sarif + + codeql: + name: Analyze with CodeQL + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: 'csharp' + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 diff --git a/packages/psdocs/.github/workflows/build.yaml b/packages/psdocs/.github/workflows/build.yaml new file mode 100644 index 00000000..6db06fc3 --- /dev/null +++ b/packages/psdocs/.github/workflows/build.yaml @@ -0,0 +1,143 @@ +# +# CI Pipeline +# + +# NOTES: +# This workflow builds and tests module updates. + +name: Build +on: + push: + branches: [main, 'release/*'] + pull_request: + branches: [main, 'release/*'] + workflow_dispatch: + +env: + DOTNET_NOLOGO: true + DOTNET_CLI_TELEMETRY_OPTOUT: true + +permissions: {} + +jobs: + build: + name: Build + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.x + + - name: Install dependencies + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Build module + shell: pwsh + timeout-minutes: 5 + run: Invoke-Build -Configuration Release + + - name: Upload module + uses: actions/upload-artifact@v4 + with: + name: Module + path: ./out/modules/PSDocs/* + retention-days: 3 + if-no-files-found: error + + # - name: Upload Test Results + # uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: Module.DotNet.TestResults + # path: ./reports/*.trx + # retention-days: 3 + # if-no-files-found: error + + # - name: Upload PSRule Results + # uses: actions/upload-artifact@v3 + # if: always() + # with: + # name: Module.PSRule.TestResults + # path: ./reports/ps-rule*.xml + # retention-days: 3 + # if-no-files-found: error + + test: + name: Test (${{ matrix.rid }}-${{ matrix.shell }}) + runs-on: ${{ matrix.os }} + needs: build + permissions: + contents: read + + strategy: + # Get full test results from all platforms. + fail-fast: false + + matrix: + os: ['ubuntu-latest'] + rid: ['linux-x64'] + shell: ['pwsh'] + include: + - os: windows-latest + rid: win-x64 + shell: pwsh + - os: windows-latest + rid: win-x64 + shell: powershell + - os: ubuntu-latest + rid: linux-x64 + shell: pwsh + - os: ubuntu-latest + rid: linux-musl-x64 + shell: pwsh + - os: macos-latest + rid: osx-x64 + shell: pwsh + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.x + + - if: ${{ matrix.shell == 'pwsh' }} + name: Install dependencies (PowerShell) + shell: pwsh + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - if: ${{ matrix.shell == 'powershell' }} + name: Install dependencies (Windows PowerShell) + shell: powershell + timeout-minutes: 3 + run: ./scripts/pipeline-deps.ps1 + + - name: Download module + uses: actions/download-artifact@v4 + with: + name: Module + path: ./out/modules/PSDocs + + - if: ${{ matrix.shell == 'pwsh' }} + name: Test module (PowerShell) + shell: pwsh + timeout-minutes: 15 + run: Invoke-Build TestModule -Configuration Release + + - if: ${{ matrix.shell == 'powershell' }} + name: Test module (Windows PowerShell) + shell: powershell + timeout-minutes: 30 + run: Invoke-Build TestModule -Configuration Release diff --git a/packages/psdocs/.github/workflows/first-interaction.yaml b/packages/psdocs/.github/workflows/first-interaction.yaml new file mode 100644 index 00000000..3d433558 --- /dev/null +++ b/packages/psdocs/.github/workflows/first-interaction.yaml @@ -0,0 +1,26 @@ +# +# Stale item management +# + +# NOTES: +# This workflow greets a person for their a first issue or PR. + +name: First interaction + +on: [pull_request_target, issues] + +permissions: {} + +jobs: + greeting: + name: Greeting + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/first-interaction@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + issue-message: 'Thanks for raising your first issue, the team appreciates the time you have taken 😉' + pr-message: 'Thank you for your contribution, one of the team will evaluate shortly.' diff --git a/packages/psdocs/.github/workflows/stale.yaml b/packages/psdocs/.github/workflows/stale.yaml new file mode 100644 index 00000000..5ed1a22c --- /dev/null +++ b/packages/psdocs/.github/workflows/stale.yaml @@ -0,0 +1,40 @@ +# +# Stale item management +# + +# NOTES: +# This workflow manages stale work items on the repository. + +name: Stale maintenance +on: + schedule: + - cron: '30 1 * * *' + workflow_dispatch: + +permissions: {} + +jobs: + issue: + name: Close stale issues + runs-on: ubuntu-latest + if: github.repository == 'microsoft/PSDocs' + permissions: + issues: write + steps: + - uses: actions/stale@v9 + with: + stale-issue-message: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs within 7 days. + Thank you for your contributions. + + close-issue-message: 'This issue was closed because it has not had any recent activity.' + + days-before-stale: 14 + days-before-pr-stale: -1 + + days-before-close: 7 + days-before-pr-close: -1 + + any-of-labels: 'question,duplicate,incomplete,waiting-feedback' + stale-issue-label: stale diff --git a/packages/psdocs/.gitignore b/packages/psdocs/.gitignore new file mode 100644 index 00000000..004a415c --- /dev/null +++ b/packages/psdocs/.gitignore @@ -0,0 +1,16 @@ + +.vs/ +build/ +reports/ +out/ +**/bin/ +**/obj/ +*.user +*.diagsession +src/**/*-help.xml +src/**/*.help.txt +BenchmarkDotNet.Artifacts/ +PSDocs.Benchmark*.log +.idea/.idea.PSDocs/.idea/.gitignore +.idea/.idea.PSDocs/.idea/indexLayout.xml +.idea/.idea.PSDocs/.idea/vcs.xml diff --git a/packages/psdocs/.markdownlint.json b/packages/psdocs/.markdownlint.json new file mode 100644 index 00000000..243930ff --- /dev/null +++ b/packages/psdocs/.markdownlint.json @@ -0,0 +1,67 @@ +{ + "default": true, + "header-increment": true, + "first-header-h1": { + "level": 1 + }, + "header-style": { + "style": "atx" + }, + "ul-style": { + "style": "dash" + }, + "list-indent": true, + "ul-start-left": true, + "ul-indent": { + "indent": 2 + }, + "no-trailing-spaces": true, + "no-hard-tabs": true, + "no-reversed-links": true, + "no-multiple-blanks": true, + "line-length": { + "line_length": 100, + "code_blocks": false, + "tables": false, + "headers": true + }, + "commands-show-output": true, + "no-missing-space-atx": true, + "no-multiple-space-atx": true, + "no-missing-space-closed-atx": true, + "no-multiple-space-closed-atx": true, + "blanks-around-headers": true, + "header-start-left": true, + "no-duplicate-header": true, + "single-h1": true, + "no-trailing-punctuation": { + "punctuation": ".,;:!" + }, + "no-multiple-space-blockquote": true, + "no-blanks-blockquote": true, + "ol-prefix": { + "style": "one_or_ordered" + }, + "list-marker-space": true, + "blanks-around-fences": true, + "blanks-around-lists": true, + "no-bare-urls": true, + "hr-style": { + "style": "---" + }, + "no-emphasis-as-header": true, + "no-space-in-emphasis": true, + "no-space-in-code": true, + "no-space-in-links": true, + "fenced-code-language": false, + "first-line-h1": false, + "no-empty-links": true, + "proper-names": { + "names": [ + "PowerShell", + "JavaScript" + ], + "code_blocks": false + }, + "no-alt-text": true +} diff --git a/packages/psdocs/.playps.yml b/packages/psdocs/.playps.yml new file mode 100644 index 00000000..c2c5d54d --- /dev/null +++ b/packages/psdocs/.playps.yml @@ -0,0 +1,6 @@ + +markdown: + # Should a line break be added after headers + sectionFormat: LineBreakAfterHeader + # Set the default infostring for fenced sections + infoString: text diff --git a/packages/psdocs/.vscode/launch.json b/packages/psdocs/.vscode/launch.json new file mode 100644 index 00000000..c9a03365 --- /dev/null +++ b/packages/psdocs/.vscode/launch.json @@ -0,0 +1,45 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File", + "script": "${file}", + "args": [], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File in Temporary Console", + "script": "${file}", + "args": [], + "cwd": "${file}", + "createTemporaryIntegratedConsole": true + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Launch Current File w/Args Prompt", + "script": "${file}", + "args": [ + "${command:SpecifyScriptArgs}" + ], + "cwd": "${file}" + }, + { + "type": "PowerShell", + "request": "attach", + "name": "PowerShell Attach to Host Process", + "processId": "${command:PickPSHostProcess}", + "runspaceId": 1 + }, + { + "type": "PowerShell", + "request": "launch", + "name": "PowerShell Interactive Session", + "cwd": "${workspaceRoot}" + } + ] +} \ No newline at end of file diff --git a/packages/psdocs/.vscode/settings.json b/packages/psdocs/.vscode/settings.json new file mode 100644 index 00000000..28e939fd --- /dev/null +++ b/packages/psdocs/.vscode/settings.json @@ -0,0 +1,34 @@ +{ + "files.exclude": { + ".vs/": true, + "out/": true, + "reports/": true, + "**/bin/": true, + "**/obj/": true + }, + "yaml.format.singleQuote": true, + "yaml.schemas": { + "./schemas/PSDocs-options.schema.json": [ + "/tests/PSDocs.Tests/PSDocs.*.yml", + "/ps-docs.yaml" + ], + "./schemas/PSDocs-language.schema.json": [ + "/tests/PSDocs.Tests/**.Doc.yaml" + ] + }, + "[yaml]": { + "editor.tabSize": 2 + }, + "[markdown]": { + "editor.tabSize": 2 + }, + "files.associations": { + "**/.azure-pipelines/**/*.yaml": "azure-pipelines" + }, + "cSpell.words": [ + "cmdlet", + "cmdlets", + "hashtable", + "runspace" + ], +} diff --git a/packages/psdocs/.vscode/tasks.json b/packages/psdocs/.vscode/tasks.json new file mode 100644 index 00000000..6f2cbb84 --- /dev/null +++ b/packages/psdocs/.vscode/tasks.json @@ -0,0 +1,109 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Test", + "detail": "Build and run unit tests.", + "type": "shell", + "command": "Invoke-Build Test", + "group": { + "kind": "test", + "isDefault": true + }, + "problemMatcher": [ + "$pester" + ], + "presentation": { + "panel": "dedicated", + "clear": true + } + }, + { + "label": "Run Pester test group", + "detail": "Runs a specific group for Pester tests.", + "type": "shell", + "command": "Invoke-Build Test -TestGroup '${input:pesterTestGroup}'", + "group": "test", + "problemMatcher": [ + "$pester" + ], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "label": "coverage", + "type": "shell", + "command": "Invoke-Build Test -CodeCoverage", + "problemMatcher": [ + "$pester" + ] + }, + { + "label": "Build", + "detail": "Build module.", + "type": "shell", + "command": "Invoke-Build Build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + { + "label": "build-docs", + "type": "shell", + "command": "Invoke-Build BuildHelp", + "problemMatcher": [] + }, + { + "label": "Scaffold docs", + "detail": "Generate cmdlet markdown docs.", + "type": "shell", + "command": "Invoke-Build ScaffoldHelp", + "problemMatcher": [] + }, + { + "label": "Clean", + "detail": "Clean up temporary working paths.", + "type": "shell", + "command": "Invoke-Build Clean", + "problemMatcher": [] + }, + { + "label": "benchmark", + "type": "shell", + "command": "Invoke-Build Benchmark", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } + }, + { + "type": "PSRule", + "problemMatcher": [ + "$PSRule" + ], + "label": "PSRule: Run analysis", + "detail": "Run repository analysis.", + "modules": [ + "PSRule.Rules.MSFT.OSS" + ], + "presentation": { + "panel": "dedicated", + "clear": true + } + } + ], + "inputs": [ + { + "id": "pesterTestGroup", + "type": "promptString", + "description": "A group to use for Pester tests." + } + ] +} \ No newline at end of file diff --git a/packages/psdocs/CHANGELOG.md b/packages/psdocs/CHANGELOG.md new file mode 100644 index 00000000..a567516a --- /dev/null +++ b/packages/psdocs/CHANGELOG.md @@ -0,0 +1,444 @@ +# Change Log + +## Unreleased + +What's changed since v0.9.0: +- Update maintainer. +- Engineering: + - CI update Pester to v5.6.1 + - Bump BenchmarkDotNet to v0.14.0. + - Bump Microsoft.CodeCoverage to v17.10.0. + - Bump Microsoft.NET.Test.Sdk to v17.10.0. + - Bump xunit to v2.9.0. + - Bump xunit.runner.visualstudio to v2.5.7. + - Bump YamlDotNet to v13.7.1. + [#284](https://github.com/microsoft/PSDocs/pull/284) + - Bump Newtonsoft.Json to v13.0.3. + [#284](https://github.com/microsoft/PSDocs/pull/284) + +## v0.9.0 + +What's changed since v0.8.0: + +- Engine features: + - Added support for reading objects from file. + [#132](https://github.com/Microsoft/PSDocs/issues/132) [#131](https://github.com/Microsoft/PSDocs/issues/131) + - Added support for conditionally processing documents based on target object. + [#133](https://github.com/Microsoft/PSDocs/issues/133) + - **Breaking change**: Documents that do not set a body are skipped. + - Conditionally process target objects with script block or selector based conditions. + - Script block based conditions are PowerShell code that can be added to `Document` blocks with `-If`. + - Selector block based conditions are YAML filters that can be added to `Document` blocks with `-With`. + - Added options for configuring processing of input. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. +- General improvements: + - Added string selector conditions. + [#178](https://github.com/microsoft/PSDocs/issues/178) + - Use `startWith`, `contains`, and `endsWith` to check for a sub-string. + - Use `isString`, `isLower`, and `isUpper` to check for string type and casing. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. + - Added schema for PSDocs configuration options within `ps-docs.yaml`. + [#113](https://github.com/Microsoft/PSDocs/issues/113) + - Added support for document data and metadata in `end` convention blocks. + [#148](https://github.com/Microsoft/PSDocs/issues/148) +- Engineering: + - Migrated PSDocs to Microsoft GitHub organization. + [#145](https://github.com/microsoft/PSDocs/issues/145) + - Bump YamlDotNet to v11.2.1. + [#168](https://github.com/microsoft/PSDocs/pull/168) +- Bug fixes: + - Fixed PowerShell command completion `Document` keyword. + [#175](https://github.com/Microsoft/PSDocs/issues/175) + +What's changed since pre-release v0.9.0-B2107020: + +- No additional changes. + +See [upgrade notes](docs/upgrade-notes.md) for helpful information when upgrading from previous versions. + +## v0.9.0-B2107020 (pre-release) + +What's changed since pre-release v0.9.0-B2107015: + +- General improvements: + - Added string selector conditions. + [#178](https://github.com/microsoft/PSDocs/issues/178) + - Use `startWith`, `contains`, and `endsWith` to check for a sub-string. + - Use `isString`, `isLower`, and `isUpper` to check for string type and casing. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. + +## v0.9.0-B2107015 (pre-release) + +What's changed since pre-release v0.9.0-B2107010: + +- Bug fixes: + - Fixed failed to get document definitions with selectors. + [#174](https://github.com/Microsoft/PSDocs/issues/174) + - Fixed PowerShell command completion `Document` keyword. + [#175](https://github.com/Microsoft/PSDocs/issues/175) + +## v0.9.0-B2107010 (pre-release) + +What's changed since pre-release v0.9.0-B2107002: + +- Engine features: + - Added support for reading objects from file. + [#132](https://github.com/Microsoft/PSDocs/issues/132) + [#131](https://github.com/Microsoft/PSDocs/issues/131) + - Added support for conditionally processing documents based on target object. + [#133](https://github.com/Microsoft/PSDocs/issues/133) + - **Breaking change**: Documents that do not set a body are skipped. + - Conditionally process target objects with script block or selector based conditions. + - Script block based conditions are PowerShell code that can be added to `Document` blocks with `-If`. + - Selector block based conditions are YAML filters that can be added to `Document` blocks with `-With`. + - Added options for configuring processing of input. + - See [about_PSDocs_Selectors] and [about_PSDocs_Options] for more details. +- General improvements: + - Added schema for PSDocs configuration options within `ps-docs.yaml`. + [#113](https://github.com/Microsoft/PSDocs/issues/113) + +See [upgrade notes](docs/upgrade-notes.md) for helpful information when upgrading from previous versions. + +## v0.9.0-B2107002 (pre-release) + +What's changed since pre-release v0.9.0-B2106004: + +- Engineering: + - Migrated PSDocs to Microsoft GitHub organization. + [#145](https://github.com/microsoft/PSDocs/issues/145) + - Bump YamlDotNet dependency to v11.2.1. + [#168](https://github.com/microsoft/PSDocs/pull/168) + +## v0.9.0-B2106004 (pre-release) + +What's changed since pre-release v0.9.0-B2102002: + +- Engineering: + - Bump YamlDotNet dependency to v11.2.0. + [#165](https://github.com/Microsoft/PSDocs/pull/165) + +## v0.9.0-B2102002 (pre-release) + +What's changed since v0.8.0: + +- General improvements: + - Added support for document data and metadata in `end` convention blocks. + [#148](https://github.com/Microsoft/PSDocs/issues/148) + +## v0.8.0 + +What's changed since v0.7.0: + +- Engine features: + - Added support for running custom actions using conventions. + [#18](https://github.com/Microsoft/PSDocs/issues/18) + [#120](https://github.com/Microsoft/PSDocs/issues/120) + - Conventions provide `Begin`, `Process` and `End` blocks to hook into the document pipeline. + - Name or change the output path of documents in `Begin` and `Process` blocks. + - Generate table of contents (TOC) and perform publishing actions in `End` blocks. + - See [about_PSDocs_Conventions] for more details. + - Added support for custom configuration key values. + [#121](https://github.com/Microsoft/PSDocs/issues/121) + - See [about_PSDocs_Configuration] for more details. +- General improvements: + - Improve handling when an empty document title is set. + [#122](https://github.com/Microsoft/PSDocs/issues/122) + - Added `-Replace` parameter to `Include` keyword to replace tokens in included file. + [#134](https://github.com/Microsoft/PSDocs/issues/134) + - A hashtable of replacement tokens can be specified to replace contents within original file. + - See [about_PSDocs_Keywords] for more details. +- Bug fixes: + - Fixed boolean string conversion with the `GetBoolOrDefault` configuration helper. + [#140](https://github.com/Microsoft/PSDocs/issues/140) + - Fixed use of error action preference with `Include` keyword. + [#127](https://github.com/Microsoft/PSDocs/issues/127) + +What's changed since pre-release v0.8.0-B2102012: + +- No additional changes. + +## v0.8.0-B2102012 (pre-release) + +What's changed since pre-release v0.8.0-B2101011: + +- Engine features: + - Added support for running custom actions using conventions. + [#18](https://github.com/Microsoft/PSDocs/issues/18) + [#120](https://github.com/Microsoft/PSDocs/issues/120) + - Conventions provide `Begin`, `Process` and `End` blocks to hook into the document pipeline. + - Name or change the output path of documents in `Begin` and `Process` blocks. + - Generate table of contents (TOC) and perform publishing actions in `End` blocks. + - See [about_PSDocs_Conventions] for more details. +- Bug fixes: + - Fixed boolean string conversion with the `GetBoolOrDefault` configuration helper. + [#140](https://github.com/Microsoft/PSDocs/issues/140) + +## v0.8.0-B2101011 (pre-release) + +What's changed since pre-release v0.8.0-B2101006: + +- General improvements: + - Added `-Replace` parameter to `Include` keyword to replace tokens in included file. + [#134](https://github.com/Microsoft/PSDocs/issues/134) + - A hashtable of replacement tokens can be specified to replace contents within original file. + - See [about_PSDocs_Keywords] for more details. + +## v0.8.0-B2101006 (pre-release) + +What's changed since v0.7.0: + +- Engine features: + - Added support for custom configuration key values. + [#121](https://github.com/Microsoft/PSDocs/issues/121) + - See [about_PSDocs_Configuration] for more details. +- General improvements: + - Improve handling when an empty document title is set. + [#122](https://github.com/Microsoft/PSDocs/issues/122) +- Bug fixes: + - Fixed use of error action preference with `Include` keyword. + [#127](https://github.com/Microsoft/PSDocs/issues/127) + +## v0.7.0 + +What's changed since v0.6.3: + +- Engine features: + - Added support for MacOS and Linux. + [#59](https://github.com/Microsoft/PSDocs/issues/59) + - Added support for using document definitions from modules. + [#81](https://github.com/Microsoft/PSDocs/issues/81) + - Added support for localized strings using the `$LocalizedData` variable. + [#91](https://github.com/Microsoft/PSDocs/issues/91) + - Automatically serialize `Code` objects to JSON and YAML. + [#93](https://github.com/Microsoft/PSDocs/issues/93) + - Use the `json`, `yaml`, or `yml` info strings to automatically serialize custom objects. + - See [about_PSDocs_Keywords] for more details. +- General improvements: + - Added configuration for setting output options. + [#105](https://github.com/Microsoft/PSDocs/issues/105) + - Added parameter alias `-MarkdownEncoding` on `New-PSDocumentOption` for `-Encoding`. + [#106](https://github.com/Microsoft/PSDocs/issues/106) + - Default the info string to `powershell` for `Code` script blocks. + [#92](https://github.com/Microsoft/PSDocs/issues/92) + - See [about_PSDocs_Keywords] for more details. +- Deprecations and removals: + - Added [upgrade notes](docs/upgrade-notes.md) for migration from v0.6.x to v0.7.0. + - **Breaking change**: Removed support for inline document blocks. + - Use `Invoke-PSDocument` with `.Doc.ps1` files instead. + - Helper functions within the script scope must be flagged with `global` scope. + - **Breaking change**: Removed script block usage of `Note` and `Warning`. + - Script block support was previously deprecated in v0.6.0. + - Use pipeline instead. + - **Breaking change**: Removed support for `-When` section parameter. + - `-When` was previously replaced with `-If` in v0.6.0. +- Engineering: + - Bump YamlDotNet dependency to v8.1.2. +- Bug fixes: + - Fixed inconsistencies with default options file name. + [#103](https://github.com/Microsoft/PSDocs/issues/103) + - Fixed line break after block quote. + [#104](https://github.com/Microsoft/PSDocs/issues/104) + +See [upgrade notes](docs/upgrade-notes.md) for helpful information when upgrading from previous versions. + +What's changed since pre-release v0.7.0-B2101015: + +- No additional changes. + +## v0.7.0-B2101015 (pre-release) + +What's changed since pre-release v0.7.0-B2008035: + +- Engine features: + - Added support for localized strings using the `$LocalizedData` variable. + [#91](https://github.com/Microsoft/PSDocs/issues/91) + - Automatically serialize `Code` objects to JSON and YAML. + [#93](https://github.com/Microsoft/PSDocs/issues/93) + - Use the `json`, `yaml`, or `yml` info strings to automatically serialize custom objects. + - See [about_PSDocs_Keywords] for more details. +- General improvements: + - Added configuration for setting output options. + [#105](https://github.com/Microsoft/PSDocs/issues/105) + - Added parameter alias `-MarkdownEncoding` on `New-PSDocumentOption` for `-Encoding`. + [#106](https://github.com/Microsoft/PSDocs/issues/106) + - Default the info string to `powershell` for `Code` script blocks. + [#92](https://github.com/Microsoft/PSDocs/issues/92) + - See [about_PSDocs_Keywords] for more details. +- Bug fixes: + - Fixed inconsistencies with default options file name. + [#103](https://github.com/Microsoft/PSDocs/issues/103) + - Fixed line break after block quote. + [#104](https://github.com/Microsoft/PSDocs/issues/104) + +## v0.7.0-B2008035 (pre-release) + +What's changed since pre-release v0.7.0-B2008022: + +- Engine features: + - Added support for using document definitions from modules. + [#81](https://github.com/Microsoft/PSDocs/issues/81) + +## v0.7.0-B2008022 (pre-release) + +What's changed since v0.6.3: + +- Engine features: + - Added support for MacOS and Linux. + [#59](https://github.com/Microsoft/PSDocs/issues/59) +- Deprecations and removals: + - Added [upgrade notes](docs/upgrade-notes.md) for migration from v0.6.x to v0.7.0. + - **Breaking change**: Removed support for inline document blocks. + - Use `Invoke-PSDocument` with `.Doc.ps1` files instead. + - Helper functions within the script scope must be flagged with `global` scope. + - **Breaking change**: Removed script block usage of `Note` and `Warning`. + - Script block support was previously deprecated in v0.6.0. + - Use pipeline instead. + - **Breaking change**: Removed support for `-When` section parameter. + - `-When` was previously replaced with `-If` in v0.6.0. +- Engineering: + - Bump YamlDotNet dependency to v8.1.2. + +See [upgrade notes](docs/upgrade-notes.md) for helpful information when upgrading from previous versions. + +## v0.6.3 + +What's changed since v0.6.2: + +- Bug fixes: + - Fix concatenation of multiple lines in Code section. + [#69](https://github.com/Microsoft/PSDocs/issues/69) + +## v0.6.2 + +What's changed since v0.6.1: + +- Bug fixes: + - Fixed PositionMessage cannot be found on this object. + [#63](https://github.com/Microsoft/PSDocs/issues/63) + - Fixed handling of null metadata hashtable. + [#60](https://github.com/Microsoft/PSDocs/issues/60) + +## v0.6.1 + +What's changed since v0.6.0: + +- Bug fixes: + - Fixed null reference for table columns with undefined properties. + [#53](https://github.com/Microsoft/PSDocs/issues/53) + +## v0.6.0 + +What's changed since v0.5.0: + +- Engine features: + - Added `BlockQuote` keyword to generate block quotes in addition to existing `Note` and `Warning` keywords which are specific to DocFX. + - See [about_PSDocs_Keywords](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#blockquote) help topic for details. + - Added `-Culture` parameter to `Invoke-PSDocument`, which allows generation of multiple localized output files. + - Added `Include` keyword to insert content from an external file. + - Use the `-UseCulture` switch of `Include` to insert content from a culture specific external file. + - See [about_PSDocs_Keywords](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#include) help topic for details. +- General improvements: + - Added new contextual help topic to provide details on automatic variables exposed for PSDocs for use within document definitions. + - Added support for locked down environments to ensure that documents are executed as constrained code when Device Guard is used. + - Use the `Execution.LanguageMode = 'ConstrainedLanguage'` option to force constrained language mode. + - **Important change**: Improved markdown formatting for tables. + [#31](https://github.com/Microsoft/PSDocs/issues/31) + - Table columns are now padded by default to match header width. + Set `Markdown.ColumnPadding` option to `None` to match format style from PSDocs <= 0.5.0. + - Pipe characters on the start and end of a table row are not added by default. + Set `Markdown.UseEdgePipes` option to `Always` to match format style from PSDocs <= 0.5.0. + - Property expressions now support Label, Expression, Alignment and Width keys. + - **Experimental**: Publishing of keywords for syntax completion with editors. +- Deprecations and removals: + - **Breaking change**: Removed `Import-PSDocumentTemplate` cmdlet. Use `Invoke-PSDocument` instead or dot source. + - **Breaking change**: Removed support for `-Function` parameter of `Invoke-PSDocument`. + External commands can be executed in document blocks. Re-evaluating if this is really needed. + - **Important change**: Renamed `-When` parameter on Section block to `-If`. + - This provides a shorter parameter and more clearly describes the intent of the parameter. + - `-When` is still works but is deprecated. + - **Important change**: Added support for `Note` and `Warning` keywords to accept text from the pipeline. + - Using the pipeline is now the preferred way to use `Note` and `Warning` keywords. + - `Note` and `Warning` script blocks are still work, but are deprecated. +- Bug fixes: + - Fixed consistency of line break generation before and after document content. + +## v0.5.0 + +What's changed since v0.4.0: + +- Engine features: + - Added support for property expressions with the `Table` keyword. + [#27](https://github.com/Microsoft/PSDocs/issues/27) + - Added support for building all document definitions from a path using the `-Path` parameter. + [#25](https://github.com/Microsoft/PSDocs/issues/25) + - Additionally document definitions can be filtered with the `-Name` and `-Tag` parameter. + - This is the recommended way to build documents going forward. + - Added support for providing options for `Invoke-PSDocument` using YAML. + - See [about_PSDocs_Options] for more details. +- General improvements: + - **Important change**: Deprecated support for using `Invoke-PSDocument` with inline document definitions. + - Improved support for using named document definitions inline, use this to call inline document definitions. + - **Breaking change**: Empty `Section` blocks are not rendered by default. + [#32](https://github.com/Microsoft/PSDocs/issues/32) + - Use `-Force` parameter on specific sections or `Markdown.SkipEmptySections = $False` option to force empty sections to be written. +- Bug fixes: + - Fixed to prevent string builder properties being outputted each time `Invoke-PSDocument` is called. + +## v0.4.0 + +What's changed since v0.3.0: + +- Engine features: + - Added `New-PSDocumentOption` cmdlet to configure document generation. + - Added `-Option` parameter to `Invoke-PSDocument` cmdlet to accept configuration options. +- General improvements: + - **Important change**: Renamed `Yaml` keyword to `Metadata`. + `Yaml` keyword is still supported but deprecated, switch to using `Metadata` instead. + - **Breaking change**: Added support for encoding markdown content output. + [#16](https://github.com/Microsoft/PSDocs/issues/16) + - To specify the encoding use the `-Encoding` parameter of `Invoke-PSDocument` and `Invoke-DscNodeDocument`. + - Output now defaults to UTF-8 without byte order mark (BOM) instead of ASCII. +- Bug fixes: + - Fixed handling of line break for multiline table columns using a wrap separator. + [#11](https://github.com/Microsoft/PSDocs/issues/11) + +## v0.3.0 + +What's changed since v0.2.0: + +- Engine features: + - Code blocks now generate fenced sections instead of indented sections. +- General improvements: + - Improved `Yaml` block handling to allow YAML header to be defined throughout the document and merged when multiple blocks are defined. + - Improved cmdlet help. + - Output path is now automatically created by `Invoke-PSDocument` if it doesn't exist. + - **Breaking change**: The body of Code blocks are now no longer evaluated as an expression. + - This change improves editing of document templates, allowing editors to complete valid PowerShell syntax. + - Define an expression and the pipe the results to the Code keyword to dynamically generate the contents of a code block. +- Engineering: + - **Breaking change**: `-ConfigurationData` parameter of `Invoke-PSDocument` has been removed while purpose and future use of the parameter is reconsidered. +- Bug fixes: + - Fixes to improve handling when `Title` block is used multiple times. + - Fixes to prevent YAML header being created when `Yaml` block is not used. + +## v0.2.0 + +What's changed since v0.1.0: + +- Engine features: + - Added Desired State Configuration (DSC) extension module `PSDocs.Dsc` to generate markdown from DSC `.mof` files. + - Added support to include documentation from external script file. +- Engineering: + - Moved markdown processor to a separate module. +- Bug fixes: + - Fixed handling of multi-line notes and warnings. + +## v0.1.0 + +- Initial release. + +[about_PSDocs_Configuration]: docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md +[about_PSDocs_Conventions]: docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md +[about_PSDocs_Keywords]: docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md +[about_PSDocs_Options]: docs/concepts/PSDocs/en-US/about_PSDocs_Options.md +[about_PSDocs_Selectors]: docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md diff --git a/packages/psdocs/CODE_OF_CONDUCT.md b/packages/psdocs/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f9ba8cf6 --- /dev/null +++ b/packages/psdocs/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/packages/psdocs/CONTRIBUTING.md b/packages/psdocs/CONTRIBUTING.md new file mode 100644 index 00000000..0dee0343 --- /dev/null +++ b/packages/psdocs/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## How to contribute + +- File or vote up issues +- Improve documentation +- Fix bugs or add features + +### Intro to Git and GitHub + +When contributing to documentation or code changes, you'll need to have a GitHub account and a basic understanding of Git. +Check out the links below to get started. + +- Make sure you have a [GitHub account][github-signup]. +- GitHub Help: + - [Git and GitHub learning resources][learn-git]. + - [GitHub Flow Guide][github-flow]. + - [Fork a repo][github-fork]. + - [About Pull Requests][github-pr]. + +## Contributing to issues + +- Check if the issue you are going to file already exists in our GitHub [issues][issue]. +- If you do not see your problem captured, please file a new issue and follow the provided template. +- If the an open issue exists for the problem you are experiencing, vote up the issue or add a comment. + +## Contributing to code + +- Before writing a fix or feature enhancement, ensure that an issue is logged. +- Be prepared to discuss a feature and take feedback. +- Include unit tests and updates documentation to complement the change. + +When you are ready to contribute a fix or feature: + +- Start by [forking the PSDocs repo][github-fork]. +- Create a new branch from main in your fork. +- Add commits in your branch. + - If you have updated module code also update `CHANGELOG.md`. + - You don't need to update the `CHANGELOG.md` for changes to unit tests or documentation. + - Try building your changes locally. See [building from source][build] for instructions. +- [Create a pull request][github-pr-create] to merge changes into the PSDocs `main` branch. + - If you are _ready_ for your changes to be reviewed create a _pull request_. + - If you are _not ready_ for your changes to be reviewed, create a _draft pull request_. + - An continuous integration (CI) process will automatically build your changes. + - You changes must build successfully to be merged. + - If you have any build errors, push new commits to your branch. + - Avoid using forced pushes or squashing changes while in review, as this makes reviewing your changes harder. + +[learn-git]: https://help.github.com/en/articles/git-and-github-learning-resources +[github-flow]: https://guides.github.com/introduction/flow/ +[github-signup]: https://github.com/signup/free +[github-fork]: https://help.github.com/en/github/getting-started-with-github/fork-a-repo +[github-pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests +[github-pr-create]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork +[build]: docs/scenarios/install-instructions.md#building-from-source +[issue]: https://github.com/Microsoft/PSDocs/issues +[discussion]: https://github.com/Microsoft/PSDocs/discussions diff --git a/packages/psdocs/GitVersion.yml b/packages/psdocs/GitVersion.yml new file mode 100644 index 00000000..aec71b05 --- /dev/null +++ b/packages/psdocs/GitVersion.yml @@ -0,0 +1,24 @@ +# +# Configure GitVersion +# + +next-version: 0.10.0 +branches: + main: + regex: ^main$ + tag: 'B' + increment: Minor + is-mainline: true + feature: + regex: ^feature/ + source-branches: [ 'main' ] + increment: Inherit + tag: B + release: + regex: ^release/ +ignore: + sha: [] +merge-message-formats: {} +tag-prefix: v +mode: ContinuousDeployment +increment: Inherit diff --git a/packages/psdocs/LICENSE b/packages/psdocs/LICENSE new file mode 100644 index 00000000..22aed37e --- /dev/null +++ b/packages/psdocs/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/psdocs/PSDocs.sln b/packages/psdocs/PSDocs.sln new file mode 100644 index 00000000..c6d73f02 --- /dev/null +++ b/packages/psdocs/PSDocs.sln @@ -0,0 +1,65 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29230.47 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSDocs", "src\PSDocs\PSDocs.csproj", "{8BD3D495-74E7-4B34-9E69-D00A1AE53008}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSDocs.Tests", "tests\PSDocs.Tests\PSDocs.Tests.csproj", "{684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSDocs.Benchmark", "src\PSDocs.Benchmark\PSDocs.Benchmark.csproj", "{1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|x64.ActiveCfg = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|x64.Build.0 = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|x86.ActiveCfg = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Debug|x86.Build.0 = Debug|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|Any CPU.Build.0 = Release|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|x64.ActiveCfg = Release|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|x64.Build.0 = Release|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|x86.ActiveCfg = Release|Any CPU + {8BD3D495-74E7-4B34-9E69-D00A1AE53008}.Release|x86.Build.0 = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|x64.ActiveCfg = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|x64.Build.0 = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|x86.ActiveCfg = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Debug|x86.Build.0 = Debug|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|Any CPU.Build.0 = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|x64.ActiveCfg = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|x64.Build.0 = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|x86.ActiveCfg = Release|Any CPU + {684AD3CB-E431-4CC5-A8AE-2B2E8CEE6B6C}.Release|x86.Build.0 = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|x64.ActiveCfg = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|x64.Build.0 = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Debug|x86.Build.0 = Debug|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|Any CPU.Build.0 = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|x64.ActiveCfg = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|x64.Build.0 = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|x86.ActiveCfg = Release|Any CPU + {1A60D079-A9A5-4C24-85F5-5EF2A6E93EAB}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {293CBC6B-EA22-460E-8294-807DF0C6008F} + EndGlobalSection +EndGlobal diff --git a/packages/psdocs/README.md b/packages/psdocs/README.md new file mode 100644 index 00000000..89274690 --- /dev/null +++ b/packages/psdocs/README.md @@ -0,0 +1,214 @@ +# PSDocs + +A PowerShell module with commands to generate markdown from objects using PowerShell syntax. + +![ci-badge] + +## Support + +This project uses GitHub Issues to track bugs and feature requests. +Please search the existing issues before filing new issues to avoid duplicates. + +- For new issues, file your bug or feature request as a new [issue]. +- For help, discussion, and support questions about using this project, join or start a [discussion]. + +Support for this project/ product is limited to the resources listed above. + +## Getting the modules + +You can download and install the PSDocs module from the PowerShell Gallery. + +Module | Description | Downloads / instructions +------ | ----------- | ------------------------ +PSDocs | Generate markdown from PowerShell | [latest][psg-psdocs] / [instructions][install] + +For integration modules see [related projects](#related-projects). + +## Getting started + +The following example shows basic PSDocs usage. +For specific use cases see [scenarios](#scenarios). + +### Define a document + +A document provides instructions on how PSDocs should render an object into documentation. +To define a document, create the `Document` script block saved to a file with the `.Doc.ps1` extension. + +For example: + +```powershell +# File: Sample.Doc.ps1 + +# Define a document called Sample +Document Sample { + # Define content here +} +``` + +Within the document body provide one or more instructions. + +For example: + +```powershell +# File: Sample.Doc.ps1 + +# Define a document called Sample +Document Sample { + + # Add an introduction section + Section Introduction { + # Add a comment + "This is a sample file list from $TargetObject" + + # Generate a table + Get-ChildItem -Path $TargetObject | Table -Property Name,PSIsContainer + } +} +``` + +### Execute a document + +To execute the document use `Invoke-PSDocument`. + +For example: + +```powershell +Invoke-PSDocument -InputObject 'C:\'; +``` + +An example of the output generated is available [here](docs/examples/Get-child-item-output.md). + +### Scenarios + +- [Azure Resource Manager template example](docs/scenarios/arm-template/arm-template.md) +- [Integration with DocFX](docs/scenarios/docfx/integration-with-docfx.md) + +## Language reference + +PSDocs extends PowerShell with domain specific language (DSL) keywords and cmdlets. + +### Keywords + +The following language keywords are used by the `PSDocs` module: + +- [Document](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#document) - Defines a named documentation block +- [Section](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#section) - Creates a named section +- [Title](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#title) - Sets the document title +- [Code](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#code) - Inserts a block of code +- [BlockQuote](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#blockquote) - Inserts a block quote +- [Note](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#note) - Inserts a note using DocFx formatted markdown (DFM) +- [Warning](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#warning) - Inserts a warning using DocFx formatted markdown (DFM) +- [Metadata](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#metadata) - Inserts a yaml header +- [Table](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#table) - Inserts a table from pipeline objects +- [Include](docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md#include) - Insert content from an external file + +### Commands + +The following commands exist in the `PSDocs` module: + +- [Invoke-PSDocument](docs/commands/PSDocs/en-US/Invoke-PSDocument.md) +- [Get-PSDocument](docs/commands/PSDocs/en-US/Get-PSDocument.md) +- [Get-PSDocumentHeader](docs/commands/PSDocs/en-US/Get-PSDocumentHeader.md) +- [New-PSDocumentOption](docs/commands/PSDocs/en-US/New-PSDocumentOption.md) + +The following commands exist in the `PSDocs.Dsc` module: + +- [Get-DscMofDocument](docs/commands/PSDocs.Dsc/en-US/Get-DscMofDocument.md) +- [Invoke-DscNodeDocument](docs/commands/PSDocs.Dsc/en-US/Invoke-DscNodeDocument.md) + +### Concepts + +The following conceptual topics exist in the `PSDocs` module: + +- [Configuration](docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md) +- [Conventions](docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md) +- [Options](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md) + - [Configuration](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#configuration) + - [Execution.LanguageMode](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#executionlanguagemode) + - [Markdown.ColumnPadding](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdowncolumnpadding) + - [Markdown.Encoding](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownencoding) + - [Markdown.SkipEmptySections](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownskipemptysections) + - [Markdown.UseEdgePipes](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownuseedgepipes) + - [Markdown.WrapSeparator](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownwrapseparator) + - [Output.Culture](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#outputculture) + - [Output.Path](docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#outputpath) +- [Selectors](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md) + - [AllOf](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#allof) + - [AnyOf](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#anyof) + - [Contains](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#contains) + - [Equals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#equals) + - [EndsWith](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#endswith) + - [Exists](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#exists) + - [Field](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#field) + - [Greater](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greater) + - [GreaterOrEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greaterorequals) + - [HasValue](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#hasvalue) + - [In](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#in) + - [IsLower](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#islower) + - [IsString](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isstring) + - [IsUpper](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isupper) + - [Less](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#less) + - [LessOrEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#lessorequals) + - [Match](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#match) + - [Not](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#not) + - [NotEquals](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notequals) + - [NotIn](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notin) + - [NotMatch](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notmatch) + - [StartsWith](docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#startswith) +- [Variables](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md) + - [$Culture](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#culture) + - [$Document](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#document) + - [$InstanceName](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#instancename) + - [$LocalizedData](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#localizeddata) + - [$PSDocs](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#psdocs) + - [$TargetObject](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#targetobject) + - [$Section](docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md#section) + +## Related projects + +The following projects use or integrate with PSDocs. + +Name | Description +---- | ----------- +[PSDocs.Azure] | Generate documentation from Azure infrastructure as code (IaC) artifacts. +[PSDocs.Dsc] | Extension for PSDocs to generate markdown from Desired State Configuration. + +## Changes and versioning + +Modules in this repository will use the [semantic versioning](http://semver.org/) model to declare breaking changes from v1.0.0. +Prior to v1.0.0, breaking changes may be introduced in minor (0.x.0) version increments. +For a list of module changes please see the [change log](CHANGELOG.md). + +> Pre-release module versions are created on major commits and can be installed from the PowerShell Gallery. +> Pre-release versions should be considered experimental. +> Modules and change log details for pre-releases will be removed as standard releases are made available. + +## Contributing + +This project welcomes contributions and suggestions. +If you are ready to contribute, please visit the [contribution guide](CONTRIBUTING.md). + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Maintainers + +- [Bernie White](https://github.com/BernieWhite) +- [Vic Perdana](https://github.com/vicperdana) + +## License + +This project is [licensed under the MIT License](LICENSE). + +[install]: docs/install-instructions.md +[issue]: https://github.com/Microsoft/PSDocs/issues +[discussion]: https://github.com/Microsoft/PSDocs/discussions +[ci-badge]: https://bewhite.visualstudio.com/PSDocs/_apis/build/status/PSDocs-CI?branchName=main +[psg-psdocs]: https://www.powershellgallery.com/packages/PSDocs +[psg-psdocs-version-badge]: https://img.shields.io/powershellgallery/v/PSDocs.svg +[psg-psdocs-installs-badge]: https://img.shields.io/powershellgallery/dt/PSDocs.svg +[PSDocs.Dsc]: https://www.powershellgallery.com/packages/PSDocs.Dsc +[PSDocs.Azure]: https://azure.github.io/PSDocs.Azure/ diff --git a/packages/psdocs/SECURITY.md b/packages/psdocs/SECURITY.md new file mode 100644 index 00000000..62b7d2ce --- /dev/null +++ b/packages/psdocs/SECURITY.md @@ -0,0 +1,43 @@ +# Security policy + + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + diff --git a/packages/psdocs/SUPPORT.md b/packages/psdocs/SUPPORT.md new file mode 100644 index 00000000..4bf3324e --- /dev/null +++ b/packages/psdocs/SUPPORT.md @@ -0,0 +1,16 @@ +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. +Please search the existing issues before filing new issues to avoid duplicates. + +- For new issues, file your bug or feature request as a new [issue]. +- For help, discussion, and support questions about using this project, join or start a [discussion]. + +## Microsoft Support Policy + +Support for this project/ product is limited to the resources listed above. + +[issue]: https://github.com/Microsoft/PSDocs/issues +[discussion]: https://github.com/Microsoft/PSDocs/discussions diff --git a/packages/psdocs/ThirdPartyNotices.txt b/packages/psdocs/ThirdPartyNotices.txt new file mode 100644 index 00000000..f3621d8b --- /dev/null +++ b/packages/psdocs/ThirdPartyNotices.txt @@ -0,0 +1,56 @@ +Do Not Translate or Localize + +This file is based on or incorporates material from the projects listed below (Third Party IP). The original copyright notice and the license under which Microsoft received such Third Party IP, are set forth below. Such licenses and notices are provided for informational purposes only. Microsoft licenses the Third Party IP to you under the licensing terms for the Microsoft product. Microsoft reserves all other rights not expressly granted under this agreement, whether by implication, estoppel or otherwise. + +--------------------------------------------- +File: YamlDotNet +--------------------------------------------- + +https://github.com/aaubry/YamlDotNet + +Copyright (c) 2008, 2009, 2010, 2011, 2012, 2013, 2014 Antoine Aubry and contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +--------------------------------------------- +File: Newtonsoft.Json +--------------------------------------------- + +https://github.com/JamesNK/Newtonsoft.Json + +The MIT License (MIT) + +Copyright (c) 2007 James Newton-King + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/psdocs/build.ps1 b/packages/psdocs/build.ps1 new file mode 100644 index 00000000..4ef47319 --- /dev/null +++ b/packages/psdocs/build.ps1 @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Note: +# This manually builds the project locally + +. ./scripts/pipeline-deps.ps1 +Invoke-Build Test + +Write-Host 'If no build errors occurred. The module has been saved to out/modules/PSDocs' diff --git a/packages/psdocs/docs/commands/.markdownlint.json b/packages/psdocs/docs/commands/.markdownlint.json new file mode 100644 index 00000000..e88d2244 --- /dev/null +++ b/packages/psdocs/docs/commands/.markdownlint.json @@ -0,0 +1,4 @@ +{ + "first-line-h1": false, + "no-bare-urls": false +} \ No newline at end of file diff --git a/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocument.md b/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocument.md new file mode 100644 index 00000000..8122589b --- /dev/null +++ b/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocument.md @@ -0,0 +1,142 @@ +--- +external help file: PSDocs-help.xml +Module Name: PSDocs +online version: https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Get-PSDocument.md +schema: 2.0.0 +--- + +# Get-PSDocument + +## SYNOPSIS + +Get document definitions. + +## SYNTAX + +```text +Get-PSDocument [-Module ] [-ListAvailable] [-Name ] [[-Path] ] + [-Option ] [] +``` + +## DESCRIPTION + +Gets a list of document definitions from paths and modules. +Document definitions are discovered within files ending in `.Doc.ps1`. +By default, definitions will be be discovered from the current working path. +Use `-Module` to discover definitions from modules. + +A document is defined using the `Document` keyword. + +## EXAMPLES + +### Example 1 + +```powershell +Get-PSDocument; +``` + +Get a list of document definitions from the current working path. + +## PARAMETERS + +### -Module + +List document definitions in the specified modules. +When specified, only document definitions from modules will be listed. +To additionally list document definitions in paths use `-Path` together with `-Module`. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: m + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -ListAvailable + +Get document definitions from all modules even ones that are not imported. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path + +A list of paths to check for document definitions. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: p + +Required: False +Position: 1 +Default value: $PWD +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Name + +The name of specific document definitions to return. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: n + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Option + +Additional options that configure PSDocs. +A `PSDocumentOption` can be created by using the `New-PSDocumentOption` cmdlet. +Alternatively a hashtable or path to YAML file can be specified with options. + +```yaml +Type: PSDocumentOption +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSDocs.Definitions.IDocumentDefinition + +An instance of a document definition. + +## NOTES + +## RELATED LINKS + +[Invoke-PSDocument](Invoke-PSDocument.md) diff --git a/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocumentHeader.md b/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocumentHeader.md new file mode 100644 index 00000000..2972870e --- /dev/null +++ b/packages/psdocs/docs/commands/PSDocs/en-US/Get-PSDocumentHeader.md @@ -0,0 +1,85 @@ +--- +external help file: PSDocs-help.xml +Module Name: PSDocs +online version: https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Get-PSDocumentHeader.md +schema: 2.0.0 +--- + +# Get-PSDocumentHeader + +## SYNOPSIS + +Get the Yaml header from a PSDocs generated markdown file. + +## SYNTAX + +```text +Get-PSDocumentHeader [[-Path] ] [] +``` + +## DESCRIPTION + +Get the Yaml header from a PSDocs generated markdown file. + +## EXAMPLES + +### Example 1 + +```powershell +PS C:\> Get-PSDocumentHeader -Path '.\build\Default'; +``` + +Get the Yaml header for all markdown files in the Default directory. + +### Example 2 + +```powershell +PS C:\> Get-PSDocumentHeader -Path '.\build\Default\Server1.md'; +``` + +Get the Yaml header for a specific file Server1.md. + +### Example 3 + +```powershell +PS C:\> Get-PSDocumentHeader; +``` + +Get the Yaml header for all markdown files in the current working directory. + +## PARAMETERS + +### -Path + +The path to a specific markdown file or a parent directory containing one or more markdown files. +A trailing slash is not required. + +If a path is not specified the current working path will be used. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: FullName + +Required: False +Position: 0 +Default value: $PWD +Accept pipeline input: True (ByPropertyName) +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.String + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS diff --git a/packages/psdocs/docs/commands/PSDocs/en-US/Invoke-PSDocument.md b/packages/psdocs/docs/commands/PSDocs/en-US/Invoke-PSDocument.md new file mode 100644 index 00000000..a43fd728 --- /dev/null +++ b/packages/psdocs/docs/commands/PSDocs/en-US/Invoke-PSDocument.md @@ -0,0 +1,370 @@ +--- +external help file: PSDocs-help.xml +Module Name: PSDocs +online version: https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md +schema: 2.0.0 +--- + +# Invoke-PSDocument + +## SYNOPSIS + +Create markdown from an input object. + +## SYNTAX + +### Input (Default) + +```text +Invoke-PSDocument [-Module ] [-Name ] [-Tag ] [-InstanceName ] + [-InputObject ] [[-Path] ] [-Format ] [-OutputPath ] [-PassThru] + [-Option ] [-Encoding ] [-Culture ] [-Convention ] + [] +``` + +### InputPath + +```text +Invoke-PSDocument -InputPath [-Module ] [-Name ] [-Tag ] + [-InstanceName ] [[-Path] ] [-Format ] [-OutputPath ] [-PassThru] + [-Option ] [-Encoding ] [-Culture ] [-Convention ] + [] +``` + +## DESCRIPTION + +Create markdown from an input object using a document definition. +Document definitions are discovered within files ending in `.Doc.ps1`. +By default, definitions will be be discovered from the current working path. +Use `-Module` to discover definitions from modules. + +A document is defined using the `Document` keyword. + +## EXAMPLES + +### Example 1 + +```powershell +# Create a new document definition called Sample in Sample.Doc.ps1 +Set-Content -Path .\Sample.Doc.ps1 -Value @' +Document Sample { + + # Add an introduction section + Section Introduction { + + # Add a comment + "This is a sample file list from $TargetObject" + + # Generate a table + Get-ChildItem -Path $TargetObject | Table -Property Name,PSIsContainer + } +} +'@ + +# Discover document definitions in the current working path (and subdirectories) within .Doc.ps1 files +Invoke-PSDocument -Path .; +``` + +Create markdown using *.Doc.ps1 files loaded from the current working directory. + +### Example 2 + +```powershell +# Create a new document definition called Sample in Sample.Doc.ps1 +Set-Content -Path .\Sample.Doc.ps1 -Value @' +Document Sample { + + $object = $InputObject + # Add an introduction section + Section $InputObject.name { + + # Add a comment + "This is a sample file list from $InputObject.folder" + + # Generate a table + Get-ChildItem -Path $InputObject.folder | Table -Property Name,PSIsContainer + } +} +'@ +#Create PSObject with info we want to pass into markdown +$PSDocsInputObject = New-Object PSObject -property @{ + 'name' = 'foldername' + 'folder' = 'C:\testfolder' +} + +# Create document based on Sample.Doc.ps1 passing PSObject +Invoke-PSDocument -Path .\Sample.Doc.ps1 -InputObject $PSDocsInputObject; +``` + +Create markdown using a Doc.ps1 file, passing a PSObject in to generate dynamic content. + +## PARAMETERS + +### -Module + +Get document definitions in the specified modules. +When specified, only document definitions from modules will be used. +To additionally use document definitions in paths use `-Path` together with `-Module`. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: m + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Name + +The name of a specific document definitions to use. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: n + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Tag + +One or more tags that the document definition must contain. +If more then one tag is specified, all tags be present on the document definition to be evaluated. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InstanceName + +The name of the resulting markdown file. +During execution of this command, the variable `$InstanceName` will be available within the document definition for use by expressions. + +If InstanceName is not specified the name of the document definition will be used instead. +If more then one InstanceName is specified, multiple markdown files will be generated in the order they were specified. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Format + +Configures the input format for when a string is passed in as a target object. + +When the `-InputObject` parameter or pipeline input is used, strings are treated as plain text by default. +Set this option to either `Yaml`, `Json`, `PowerShellData` to have PSDocs deserialize the object. + +When the `-InputPath` parameter is used with a file path or URL. +If the Detect format is used, the file extension will be used to automatically detect the format. +When `-InputPath` is not used, `Detect` is the same as `None`. + +See `about_PSDocs_Options` for details. + +This parameter takes precedence over the `Input.Format` option if set. + +```yaml +Type: InputFormat +Parameter Sets: (All) +Aliases: InputFormat +Accepted values: None, Yaml, Json, PowerShellData, Detect + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputPath + +Instead of processing objects from the pipeline, +import objects file the specified file paths. + +```yaml +Type: String[] +Parameter Sets: InputPath +Aliases: f + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObject + +An input object that will be passed to each document and can be referenced within document blocks as `$TargetObject`. + +```yaml +Type: PSObject +Parameter Sets: Input +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: True (ByValue) +Accept wildcard characters: False +``` + +### -OutputPath + +The directory path to store markdown files created based on the specified document template. +This path will be automatically created if it doesn't exist. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PassThru + +When specified generated markdown will be returned to the pipeline instead of being written to file. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Option + +Additional options that configure PSDocs. +A `PSDocumentOption` can be created by using the `New-PSDocumentOption` cmdlet. +Alternatively a hashtable or path to YAML file can be specified with options. + +```yaml +Type: PSDocumentOption +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Encoding + +Specifies the file encoding for generated markdown files. +By default, UTF-8 without byte order mark (BOM) will be used. +To use UTF-8 with BOM specify `UTF8`. + +```yaml +Type: MarkdownEncoding +Parameter Sets: (All) +Aliases: +Accepted values: Default, UTF8, UTF7, Unicode, UTF32, ASCII + +Required: False +Position: Named +Default value: Default +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path + +A list of paths to use document definitions from. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: p + +Required: False +Position: 0 +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Culture + +A list of cultures for building documents such as _en-AU_, and _en-US_. +Documents are written to culture specific subdirectories when multiple cultures are generated. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Convention + +Specifies the name of conventions to execute during document generation. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +### System.Management.Automation.PSObject + +## OUTPUTS + +### System.Object + +## NOTES + +## RELATED LINKS + +[New-PSDocumentOption](New-PSDocumentOption.md) diff --git a/packages/psdocs/docs/commands/PSDocs/en-US/New-PSDocumentOption.md b/packages/psdocs/docs/commands/PSDocs/en-US/New-PSDocumentOption.md new file mode 100644 index 00000000..9b20daa0 --- /dev/null +++ b/packages/psdocs/docs/commands/PSDocs/en-US/New-PSDocumentOption.md @@ -0,0 +1,229 @@ +--- +external help file: PSDocs-help.xml +Module Name: PSDocs +online version: https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/New-PSDocumentOption.md +schema: 2.0.0 +--- + +# New-PSDocumentOption + +## SYNOPSIS + +Create options to configure document generation. + +## SYNTAX + +### FromPath (Default) + +```text +New-PSDocumentOption [-Path ] [-Format ] [-InputObjectPath ] + [-InputPathIgnore ] [-Encoding ] [-Culture ] [-OutputPath ] + [] +``` + +### FromOption + +```text +New-PSDocumentOption -Option [-Format ] [-InputObjectPath ] + [-InputPathIgnore ] [-Encoding ] [-Culture ] [-OutputPath ] + [] +``` + +### FromDefault + +```text +New-PSDocumentOption [-Default] [-Format ] [-InputObjectPath ] + [-InputPathIgnore ] [-Encoding ] [-Culture ] [-OutputPath ] + [] +``` + +## DESCRIPTION + +The **New-PSDocumentOption** cmdlet creates an options object that can be passed to PSDocs cmdlets to configure document generation behaviour. + +## EXAMPLES + +### Example 1 + +```powershell +PS C:\> $option = New-PSDocumentOption -Option @{ 'Markdown.WrapSeparator' = '
' }; +PS C:\> Invoke-PSDocument -Name 'Sample' -Option $option; +``` + +Create markdown using the Sample documentation definition using a wrap separator of `
`. + +## PARAMETERS + +### -Option + +Additional options that configure document generation. +Option also accepts a hashtable to configure options. + +```yaml +Type: PSDocumentOption +Parameter Sets: FromOption +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path + +The path to a YAML file containing options. + +```yaml +Type: String +Parameter Sets: FromPath +Aliases: + +Required: False +Position: Named +Default value: .\ps-docs.yaml +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Default + +When specified, defaults are used for any options not overridden. + +```yaml +Type: SwitchParameter +Parameter Sets: FromDefault +Aliases: + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Format + +Sets the `Input.Format` option to configure the input format for when a string is passed in as a target object. +See about_PSDocs_Options for more information. + +```yaml +Type: InputFormat +Parameter Sets: (All) +Aliases: InputFormat +Accepted values: None, Yaml, Json, PowerShellData, Detect + +Required: False +Position: Named +Default value: Detect +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputObjectPath + +Sets the `Input.ObjectPath` option to use an object path to use instead of the pipeline object. +See about_PSDocs_Options for more information. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -InputPathIgnore + +Sets the `Input.PathIgnore` option. +If specified, files that match the path spec will not be processed. +See about_PSDocs_Options for more information. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Encoding + +Sets the option `Markdown.Encoding`. +Specifies the file encoding for generated markdown files. +By default, UTF-8 without byte order mark (BOM) will be used. +See _about_PSDocs_Options_ for more information. + +```yaml +Type: MarkdownEncoding +Parameter Sets: (All) +Aliases: MarkdownEncoding +Accepted values: Default, UTF8, UTF7, Unicode, UTF32, ASCII + +Required: False +Position: Named +Default value: Default +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Culture + +Sets the option `Output.Culture`. +Specifies a list of cultures for building documents such as _en-AU_, and _en-US_. +See _about_PSDocs_Options_ for more information. + +```yaml +Type: String[] +Parameter Sets: (All) +Aliases: OutputCulture + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -OutputPath + +Sets the option `Output.Path`. +The option specified one or more custom field bindings. +See _about_PSDocs_Options_ for more information. + +```yaml +Type: String +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### CommonParameters + +This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see [about_CommonParameters](http://go.microsoft.com/fwlink/?LinkID=113216). + +## INPUTS + +## OUTPUTS + +### PSDocs.Configuration.PSDocumentOption + +## NOTES + +## RELATED LINKS + +[Invoke-PSDocument](Invoke-PSDocument.md) diff --git a/packages/psdocs/docs/commands/PSDocs/en-US/PSDocs.md b/packages/psdocs/docs/commands/PSDocs/en-US/PSDocs.md new file mode 100644 index 00000000..2d1c279a --- /dev/null +++ b/packages/psdocs/docs/commands/PSDocs/en-US/PSDocs.md @@ -0,0 +1,31 @@ +--- +Module Name: PSDocs +Module Guid: 1f6df554-c081-40d8-9aca-32c1abe4a1b6 +Download Help Link: https://github.com/Microsoft/PSDocs +Help Version: 0.1.0.0 +Locale: en-US +--- + +# PSDocs Module + +## Description + +Generate markdown from PowerShell. + +## PSDocs Cmdlets + +### [Get-PSDocumentHeader](Get-PSDocumentHeader.md) + +Get the Yaml header from a PSDocs generated markdown file. + +### [Invoke-PSDocument](Invoke-PSDocument.md) + +Create markdown from an input object. + +### [Get-PSDocument](Get-PSDocument.md) + +Get document definitions. + +### [New-PSDocumentOption](New-PSDocumentOption.md) + +Create options to configure document generation. diff --git a/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md new file mode 100644 index 00000000..d2975e85 --- /dev/null +++ b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md @@ -0,0 +1,97 @@ +# PSDocs_Configuration + +## about_PSDocs_Configuration + +## SHORT DESCRIPTION + +Describes custom configuration that can be used within PSDocs document definitions. + +## LONG DESCRIPTION + +PSDocs lets you generate dynamic markdown documents using PowerShell blocks known as document definitions. +Document definitions can read custom configuration set at runtime or within options to change rendering. +Within a document definition, PSDocs exposes custom configuration through the `$PSDocs` automatic variable. + +### Setting configuration + +To specify custom configuration, set a property of the `configuration` object. +Configuration can be set at runtime or as YAML by configuring `ps-docs.yaml`. + +For example: + +```yaml +# Example: ps-docs.yaml + +# YAML: Using the configuration YAML property to set custom configuration 'MODULE1_KEY1' +configuration: + MODULE1_KEY1: Value1 +``` + +To ensure each custom key is unique use a prefix followed by an underscore that represent your module. +Key names are not case sensitive, however we recommend you use uppercase for consistency. + +### Reading configuration + +The `$PSDocs` automatic variable can be used within a Document definition to read configuration. +Each custom configuration key is available under the `.Configuration` property. +Additionally, several helper methods are available for advanced usage. + +Syntax: + +```powershell +$PSDocs.Configuration. +``` + +For example: + +```powershell +# Get the value of the custom configuration 'MODULE1_KEY1' +$PSDocs.Configuration.MODULE1_KEY1 +``` + +The following helper methods are available: + +- `GetStringValues(string configurationKey)` - The configuration value as an array of strings. +This helper will always returns an array of strings. +The array will be empty if the configuration key is not defined or empty. +- `GetValueOrDefault(string configurationKey, object defaultValue)` - Returns the configuration value. +When the configuration key is not defined the default value will be used instead. +- `GetBoolOrDefault(string configurationKey, bool defaultValue)` - The configuration value as a boolean. +When the configuration key is not defined the default value will be used instead. + +Syntax: + +```powershell +$PSDocs.Configuration.helper() +``` + +For example: + +```powershell +# Example using GetStringValues +$values = $PSDocs.Configuration.GetStringValues('SAMPLE_AUTHORS'); + +# Example using GetValueOrDefault +$value = $PSDocs.Configuration.GetValueOrDefault('SAMPLE_CONTENT_OWNER', 'defaultUser'); + +# Example using GetBoolOrDefault +if ($PSDocs.Configuration.GetBoolOrDefault('SAMPLE_USE_PARAMETERS_SNIPPET', $True)) { + # Execute code +} +``` + +## NOTE + +An online version of this document is available at https://github.com/Microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Configuration.md. + +## SEE ALSO + +- [Invoke-PSDocument](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md) + +## KEYWORDS + +- Configuration +- PSDocs +- GetStringValues +- GetValueOrDefault +- GetBoolOrDefault diff --git a/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md new file mode 100644 index 00000000..f206e3ab --- /dev/null +++ b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md @@ -0,0 +1,138 @@ +# PSDocs_Conventions + +## about_PSDocs_Conventions + +## SHORT DESCRIPTION + +Describes PSDocs Conventions including how to use and author them. + +## LONG DESCRIPTION + +PSDocs generates documents dynamically from input. +When generating multiple documents it is often necessary to name or annotate them in a structured manner. +Conventions achieve this by hooking into the pipeline to trigger custom actions defined in a script block. + +### Using conventions + +A convention once defined can be included by using the `-Convention` parameter of `Invoke-PSDocument`. +To use a convention specify the name of the convention by name. +For example: + +```powershell +Invoke-PSDocument -Convention 'ExampleConvention'; +``` + +If multiple conventions are specified in an array, all are executed in they are specified. +As a result, the convention specified last may override state set by earlier conventions. + +### Defining conventions + +To define a convention, add a `Export-PSDocumentConvention` block within a `.Doc.ps1` file. +When executed the `.Doc.ps1` must be in an included path or module with `-Path` or `-Module`. + +The `Export-PSDocumentConvention` block works similar to the `Document` block. +Each convention must have a unique name. +For example: + +```powershell +# Synopsis: An example convention. +Export-PSDocumentConvention 'ExampleConvention' { + # Add code here +} +``` + +### Begin Process End blocks + +Conventions define three executable blocks `Begin`, `Process`, `End` similar to a PowerShell function. +Each block is injected in a different part of the pipeline as follows: + +- `Begin` occurs before the document definition is called. +- `Process` occurs directly after the document definition is called. +- `End` occurs after all documents have been generated. + +Convention block limitations: + +- `Begin` can not use document specific variables such as `$Document`. +- `End` can not use automatic variables except `$PSDocs.Output`. + +By default, the `Process` block used. +For example: + +```powershell +# Synopsis: The default { } executes the process block +Export-PSDocumentConvention 'ExampleConvention' { + # Process block +} + +# Synopsis: With optional -Process parameter name +Export-PSDocumentConvention 'ExampleConvention' -Process { + # Process block +} +``` + +To use `Begin` or `End` explicitly add these blocks. +For example: + +```powershell +Export-PSDocumentConvention 'ExampleConvention' -Process { + # Process block +} -Begin { + # Begin block +} -End { + # End block +} +``` + +### Naming documents + +Generated document output files are named based on InstanceName. +To alter the InstanceName of a document use the `InstanceName` property. + +Syntax: + +```text +$PSDocs.Document.InstanceName = value; +``` + +For example: + +```powershell +# Synopsis: An example naming convention. +Export-PSDocumentConvention 'TestNamingConvention1' { + $PSDocs.Document.InstanceName = 'NewName'; +} +``` + +### Setting output path + +Generated document output files are named based on OutputPath. +To alter the OutputPath of a document use the `OutputPath` property. + +Syntax: + +```text +$PSDocs.Document.OutputPath = value; +``` + +For example: + +```powershell +# Synopsis: An example naming convention. +Export-PSDocumentConvention 'TestNamingConvention1' { + $newPath = Join-Path -Path $PSDocs.Document.OutputPath -ChildPath 'new'; + $PSDocs.Document.OutputPath = $newPath; +} +``` + +## NOTE + +An online version of this document is available at https://github.com/Microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Conventions.md. + +## SEE ALSO + +- [Invoke-PSDocument](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md) + +## KEYWORDS + +- Conventions +- PSDocs diff --git a/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md new file mode 100644 index 00000000..8db5271e --- /dev/null +++ b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md @@ -0,0 +1,554 @@ +# PSDocs_Options + +## about_PSDocs_Options + +## SHORT DESCRIPTION + +Describes additional options that can be used during markdown generation. + +## LONG DESCRIPTION + +PSDocs lets you use options when calling `Invoke-PSDocument` to change how documents are generated. +This topic describes what options are available, when to and how to use them. + +The following workspace options are available for use: + +- [Configuration](#configuration) +- [Execution.LanguageMode](#executionlanguagemode) +- [Markdown.ColumnPadding](#markdowncolumnpadding) +- [Markdown.Encoding](#markdownencoding) +- [Markdown.SkipEmptySections](#markdownskipemptysections) +- [Markdown.UseEdgePipes](#markdownuseedgepipes) +- [Markdown.WrapSeparator](#markdownwrapseparator) +- [Output.Culture](#outputculture) +- [Output.Path](#outputpath) + +Options can be used by: + +- Using the `-Option` parameter of `Invoke-PSDocument` with an object created with `New-PSDocumentOption`. +- Using the `-Option` parameter of `Invoke-PSDocument` with a hashtable. +- Using the `-Option` parameter of `Invoke-PSDocument` with a YAML file. +- Configuring the default options file `ps-docs.yaml`. + +As mentioned above, a options object can be created with `New-PSDocumentOption` see cmdlet help for syntax and examples. +When using a hashtable, `@{}`, one or more options can be specified with the `-Option` parameter using a dotted notation. + +For example: + +```powershell +$option = @{ 'markdown.wrapseparator' = ' '; 'markdown.encoding' = 'UTF8' }; +Invoke-PSDocument -Path . -Option $option; +``` + +`markdown.wrapseparator` is an example of an option that can be used. +Please see the following sections for other options can be used. + +Another option is to use an external file, formatted as YAML, instead of having to create an options object manually each time. +This YAML file can be used with `Invoke-PSDocument` to quickly build documentation in a repeatable way. + +YAML properties are specified using lower camel case, for example: + +```yaml +markdown: + wrapSeparator: '\' +``` + +By default PSDocs will automatically look for a file named `ps-docs.yaml` in the current working directory. +Alternatively, you can specify a YAML file in the `-Option` parameter. + +For example: + +```powershell +Invoke-PSDocument -Path . -Option '.\myconfig.yml'. +``` + +### Configuration + +Sets custom configuration for document generation. +Document definitions may allow custom configuration to be specified. +To specify custom configuration, set a property of the configuration object. + +To ensure each custom key is unique use a prefix followed by an underscore that represent your module. +Key names are not case sensitive, however we recommend you use uppercase for consistency. + +This option can be specified using: + +```powershell +# PowerShell: Using the Configuration hashtable key to set custom configuration 'MODULE1_KEY1' +$option = New-PSDocumentOption -Option @{ 'Configuration.MODULE1_KEY1' = 'Value1' } +``` + +```yaml +# YAML: Using the configuration YAML property to set custom configuration 'MODULE2_KEY1' +configuration: + MODULE2_KEY1: Value1 +``` + +### Execution.LanguageMode + +Unless PowerShell has been constrained, full language features of PowerShell are available to use within document definitions. +In locked down environments, a reduced set of language features may be desired. + +When PSDocs is executed in an environment configured for Device Guard, only constrained language features are available. + +The following language modes are available for use in PSDocs: + +- FullLanguage +- ConstrainedLanguage + +This option can be specified using: + +```powershell +# PowerShell: Using the Execution.LanguageMode hashtable key +$option = New-PSDocumentOption -Option @{ 'Execution.LanguageMode' = 'ConstrainedLanguage' } +``` + +```yaml +# YAML: Using the execution/languageMode YAML property +execution: + languageMode: ConstrainedLanguage +``` + +### Input.Format + +Configures the input format for when a string is passed in as a target object. +This option determines if the target object is deserialized into an alternative form. + +Set this option to either `Yaml`, `Json`, `PowerShellData` to deserialize as a specific format. +The `-Format` parameter will override any value set in configuration. + +When the `-InputObject` parameter or pipeline input is used, strings are treated as plain text by default. +`FileInfo` objects for supported file formats will be deserialized based on file extension. + +When the `-InputPath` parameter is used, supported file formats will be deserialized based on file extension. +The `-InputPath` parameter can be used with a file path or URL. + +The following formats are available: + +- None - Treat strings as plain text and do not deserialize files. +- Yaml - Deserialize as one or more YAML objects. +- Json - Deserialize as one or more JSON objects. +- PowerShellData - Deserialize as a PowerShell data object. +- Detect - Detect format based on file extension. This is the default. + +If the `Detect` format is used, the file extension will be used to automatically detect the format. +When the file extension can not be determined `Detect` is the same as `None`. + +Detect uses the following file extensions: + +- Yaml - `.yaml` or `.yml` +- Json - `.json` or `.jsonc` +- PowerShellData - `.psd1` + +This option can be specified using: + +```powershell +# PowerShell: Using the Format parameter +$option = New-PSDocumentOption -Format Yaml; +``` + +```powershell +# PowerShell: Using the Input.Format hashtable key +$option = New-PSDocumentOption -Option @{ 'Input.Format' = 'Yaml' }; +``` + +```yaml +# YAML: Using the input/format property +input: + format: Yaml +``` + +```bash +# Bash: Using environment variable +export PSDOCS_INPUT_FORMAT=Yaml +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSDOCS_INPUT_FORMAT: Yaml +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSDOCS_INPUT_FORMAT + value: Yaml +``` + +### Input.ObjectPath + +The object path to a property to use instead of the pipeline object. + +By default, PSDocs processes objects passed from the pipeline against selected rules. +When this option is set, instead of evaluating the pipeline object, PSDocs looks for a property of the pipeline object specified by `ObjectPath` and uses that instead. +If the property specified by `ObjectPath` is a collection/ array, then each item is evaluated separately. + +If the property specified by `ObjectPath` does not exist, PSDocs skips the object. + +This option can be specified using: + +```powershell +# PowerShell: Using the InputObjectPath parameter +$option = New-PSDocumentOption -InputObjectPath 'items'; +``` + +```powershell +# PowerShell: Using the Input.ObjectPath hashtable key +$option = New-PSDocumentOption -Option @{ 'Input.ObjectPath' = 'items' }; +``` + +```yaml +# YAML: Using the input/objectPath property +input: + objectPath: items +``` + +```bash +# Bash: Using environment variable +export PSDOCS_INPUT_OBJECTPATH=items +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSDOCS_INPUT_OBJECTPATH: items +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSDOCS_INPUT_OBJECTPATH + value: items +``` + +### Input.PathIgnore + +Ignores input files that match the path spec when using `-InputPath`. +If specified, files that match the path spec will not be processed. +By default, all files are processed. + +This option can be specified using: + +```powershell +# PowerShell: Using the InputPathIgnore parameter +$option = New-PSDocumentOption -InputPathIgnore '*.Designer.cs'; +``` + +```powershell +# PowerShell: Using the Input.PathIgnore hashtable key +$option = New-PSDocumentOption -Option @{ 'Input.PathIgnore' = '*.Designer.cs' }; +``` + +```yaml +# YAML: Using the input/pathIgnore property +input: + pathIgnore: + - '*.Designer.cs' +``` + +```bash +# Bash: Using environment variable +export PSDOCS_INPUT_PATHIGNORE=*.Designer.cs +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSDOCS_INPUT_PATHIGNORE: '*.Designer.cs' +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSDOCS_INPUT_PATHIGNORE + value: '*.Designer.cs' +``` + +### Markdown.ColumnPadding + +Sets how table column padding should be handled in markdown. +This doesn't affect how tables are rendered but can greatly assist readability of markdown source files. + +The following padding options are available: + +- None - No padding will be used and column values will directly follow table pipe (`|`) column separators. +- Single - A single space will be used to pad the column value. +- MatchHeader - Will pad the header with a single space, then pad the column value, to the same width as the header (default). + +When a column is set to a specific width with a property expression, `MatchHeader` will be ignored. +Columns without a width set will apply `MatchHeader` as normal. + +Example markdown using `None`: + +```markdown +|Name|Value| +|----|-----| +|Mon|Key| +|Really long name|Really long value| +``` + +Example markdown using `Single`: + +```markdown +| Name | Value | +| ---- | ----- | +| Mon | Key | +| Really long name | Really long value | +``` + +Example markdown using `MatchHeader`: + +```markdown +| Name | Value | +| ---- | ----- | +| Mon | Key | +| Really long name | Really long value | +``` + +This option can be specified using: + +```powershell +# PowerShell: Using the Markdown.ColumnPadding hashtable key +$option = New-PSDocumentOption -Option @{ 'Markdown.ColumnPadding' = 'MatchHeader' } +``` + +```yaml +# YAML: Using the markdown/columnPadding YAML property +markdown: + columnPadding: MatchHeader +``` + +### Markdown.Encoding + +Sets the text encoding used for markdown output files. +One of the following values can be used: + +- Default +- UTF8 +- UTF7 +- Unicode +- UTF32 +- ASCII + +By default `Default` is used which is UTF-8 without byte order mark (BOM) is used. + +Use this option with `Invoke-PSDocument`. +When the `-Encoding` parameter is used, it will override any value set in configuration. + +This option can be specified using: + +```powershell +# PowerShell: Using the Encoding parameter +$option = New-PSDocumentOption -Encoding 'UTF8'; +``` + +```powershell +# PowerShell: Using the Markdown.Encoding hashtable key +$option = New-PSDocumentOption -Option @{ 'Markdown.Encoding' = 'UTF8' } +``` + +```yaml +# YAML: Using the markdown/encoding YAML property +markdown: + encoding: UTF8 +``` + +Prior to PSDocs v0.4.0 the only encoding supported was ASCII. + +### Markdown.SkipEmptySections + +From PSDocs v0.5.0 onward, `Section` blocks that are empty are omitted from markdown output by default. +i.e. `Markdown.SkipEmptySections` is `$True`. + +To include empty sections (the same as PSDocs v0.4.0 or older) in markdown output either use the `-Force` parameter on a specific `Section` block or set the option `Markdown.SkipEmptySections` to `$False`. + +This option can be specified using: + +```powershell +# PowerShell: Using the Markdown.SkipEmptySections hashtable key +$option = New-PSDocumentOption -Option @{ 'Markdown.SkipEmptySections' = $False } +``` + +```yaml +# YAML: Using the markdown/skipEmptySections YAML property +markdown: + skipEmptySections: false +``` + +### Markdown.UseEdgePipes + +This option determines when pipes on the edge of a table should be used. +This option can improve readability of markdown source files, but may not be supported by all markdown renderers. + +Edge pipes are always required if the table has a single column, so this option only applies for tables with more then one column. + +The following options for edge pipes are: + +- WhenRequired - Will not use edge pipes for tables with more when one column (default). +- Always - Will always use edge pipes. + +Example markdown using `WhenRequired`: + +```markdown +Name|Value +----|----- +Mon|Key +``` + +Example markdown using `Always`: + +```markdown +|Name|Value| +|----|-----| +|Mon|Key| +``` + +Example markdown using `WhenRequired` and column padding of `MatchHeader`: + +```markdown +Name | Value +---- | ----- +Mon | Key +``` + +This option can be specified using: + +```powershell +# PowerShell: Using the Markdown.UseEdgePipes hashtable key +$option = New-PSDocumentOption -Option @{ 'Markdown.UseEdgePipes' = 'WhenRequired' } +``` + +```yaml +# YAML: Using the markdown/useEdgePipes YAML property +markdown: + useEdgePipes: WhenRequired +``` + +### Markdown.WrapSeparator + +This option specifies the character/string to use when wrapping lines in a table cell. +When a table cell contains CR and LF characters, these characters must be substituted so that the table in rendered correctly because they also have special meaning in markdown. + +By default a single space is used. +However different markdown parsers may be able to natively render a line break using alternative combinations such as `\` or `
`. + +This option can be specified using: + +```powershell +# PowerShell: Using the Markdown.WrapSeparator hashtable key +$option = New-PSDocumentOption -Option @{ 'Markdown.WrapSeparator' = '\' } +``` + +```yaml +# YAML: Using the markdown/wrapSeparator YAML property +markdown: + wrapSeparator: '\' +``` + +### Output.Culture + +Specifies a list of cultures for building documents such as _en-AU_, and _en-US_. +Documents are written to culture specific subdirectories when multiple cultures are generated. + +Use this option with `Invoke-PSDocument`. +When the `-Culture` parameter is used, it will override any value set in configuration. + +This option can be specified using: + +```powershell +# PowerShell: Using the Output.Culture hashtable key +$option = New-PSDocumentOption -Option @{ 'Output.Culture' = 'en-US', 'en-AU' } +``` + +```yaml +# YAML: Using the output/culture YAML property +output: + culture: + - 'en-US' +``` + +### Output.Path + +Configures the directory path to store markdown files created based on the specified document template. +This path will be automatically created if it doesn't exist. + +Use this option with `Invoke-PSDocument`. +When the `-OutputPath` parameter is used, it will override any value set in configuration. + +This option can be specified using: + +```powershell +# PowerShell: Using the Output.Path hashtable key +$option = New-PSDocumentOption -Option @{ 'Output.Path' = 'out/' } +``` + +```yaml +# YAML: Using the output/path YAML property +output: + path: 'out/' +``` + +## EXAMPLES + +### Example ps-docs.yaml + +```yaml +configuration: + SAMPLE_USE_PARAMETERS_SNIPPET: true + +execution: + languageMode: ConstrainedLanguage + +# Set markdown options +markdown: + # Use UTF-8 with BOM + encoding: UTF8 + skipEmptySections: false + wrapSeparator: '\' + +output: + culture: + - 'en-US' + path: 'out/' +``` + +### Default ps-docs.yaml + +```yaml +# These are the default options. +# Only properties that differ from the default values need to be specified. +configuration: { } + +execution: + languageMode: FullLanguage + +markdown: + encoding: Default + skipEmptySections: true + wrapSeparator: ' ' + columnPadding: MatchHeader + useEdgePipes: WhenRequired + +output: + culture: [ ] + path: null +``` + +## NOTE + +An online version of this document is available at https://github.com/Microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md. + +## SEE ALSO + +- [Invoke-PSDocument](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md) +- [New-PSDocumentOption](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/New-PSDocumentOption.md) + +## KEYWORDS + +- Configuration +- Options +- Markdown +- PSDocument +- Output +- Execution diff --git a/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md new file mode 100644 index 00000000..a38f3cc7 --- /dev/null +++ b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md @@ -0,0 +1,715 @@ +# PSDocs_Selectors + +## about_PSDocs_Selectors + +## SHORT DESCRIPTION + +Describes PSDocs Selectors including how to use and author them. + +## LONG DESCRIPTION + +PSDocs executes document to validate an object from input. +When evaluating an object from input, PSDocs can use selectors to perform complex matches of an object. + +- A selector is a YAML-based expression that evaluates an object. +- Each selector is comprised of nested conditions, operators, and comparison properties. +- Selectors must use one or more available conditions with a comparison property to evaluate the object. +- Optionally a condition can be nested in an operator. +- Operators can be nested within other operators. + +The following conditions are available: + +- [Contains](#contains) +- [Equals](#equals) +- [EndsWith](#endswith) +- [Exists](#exists) +- [Greater](#greater) +- [GreaterOrEquals](#greaterorequals) +- [HasValue](#hasvalue) +- [In](#in) +- [IsLower](#islower) +- [IsString](#isstring) +- [IsUpper](#isupper) +- [Less](#less) +- [LessOrEquals](#lessorequals) +- [Match](#match) +- [NotEquals](#notequals) +- [NotIn](#notin) +- [NotMatch](#notmatch) +- [StartsWith](#startswith) + +The following operators are available: + +- [AllOf](#allof) +- [AnyOf](#anyof) +- [Not](#not) + +The following comparison properties are available: + +- [Field](#field) + +Currently the following limitations apply: + +- Selectors can only evaluate a field of the target object. +The following examples can not be evaluated by selectors: + - State variables such as `$PSDocs`. + +### Using selectors as pre-conditions + +Selectors can be referenced by name as a document pre-condition by using the `-With` parameter. +For example: + +```powershell +Document 'SampleWithSelector' -With 'BasicSelector' { + # Additional content +} +``` + +Selector pre-conditions can be used together with script block pre-conditions. +If one or more selector pre-conditions are used, they are evaluated before script block pre-conditions. + +### Defining selectors + +Selectors are defined in YAML and can be included within a module or standalone `.Doc.yaml` file. +In either case, define a selector within a file ending with the `.Doc.yaml` extension. + +Use the following template to define a selector: + +```yaml +--- +# Synopsis: {{ Synopsis }} +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: '{{ Name }}' +spec: + if: { } +``` + +Within the `if` object, one or more conditions or logical operators can be used. + +### AllOf + +The `allOf` operator is used to require all nested expressions to match. +When any nested expression does not match, `allOf` does not match. +This is similar to a logical _and_ operation. + +Syntax: + +```yaml +allOf: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleAllOf' +spec: + if: + allOf: + # Both Name and Description must exist. + - field: 'Name' + exists: true + - field: 'Description' + exists: true +``` + +### AnyOf + +The `anyOf` operator is used to require one or more nested expressions to match. +When any nested expression matches, `allOf` matches. +This is similar to a logical _or_ operation. + +Syntax: + +```yaml +anyOf: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleAnyOf' +spec: + if: + anyOf: + # Name and/ or AlternativeName must exist. + - field: 'Name' + exists: true + - field: 'AlternativeName' + exists: true +``` + +### Contains + +The `contains` condition can be used to determine if the operand contains a specified sub-string. +One or more strings to compare can be specified. + +Syntax: + +```yaml +contains: +``` + +- If the operand is a field, and the field does not exist, _contains_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleContains' +spec: + if: + anyOf: + - field: 'url' + contains: '/azure/' + - field: 'url' + contains: + - 'github.io' + - 'github.com' +``` + +### Equals + +The `equals` condition can be used to compare if a field is equal to a supplied value. + +Syntax: + +```yaml +equals: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleEquals' +spec: + if: + field: 'Name' + equals: 'TargetObject1' +``` + +### EndsWith + +The `endsWith` condition can be used to determine if the operand ends with a specified string. +One or more strings to compare can be specified. + +Syntax: + +```yaml +endsWith: +``` + +- If the operand is a field, and the field does not exist, _endsWith_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleEndsWith' +spec: + if: + anyOf: + - field: 'hostname' + endsWith: '.com' + - field: 'hostname' + endsWith: + - '.com.au' + - '.com' +``` + +### Exists + +The `exists` condition determines if the specified field exists. + +Syntax: + +```yaml +exists: +``` + +- When `exists: true`, exists will return `true` if the field exists. +- When `exists: false`, exists will return `true` if the field does not exist. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleExists' +spec: + if: + field: 'Name' + exists: true +``` + +### Field + +The comparison property `field` is used with a condition to determine field of the object to evaluate. +A field can be: + +- A property name. +- A key within a hashtable or dictionary. +- An index in an array or collection. +- A nested path through an object. + +Syntax: + +```yaml +field: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleField' +spec: + if: + field: 'Properties.securityRules[0].name' + exists: true +``` + +### Greater + +Syntax: + +```yaml +greater: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleGreater' +spec: + if: + field: 'Name' + greater: 3 +``` + +### GreaterOrEquals + +Syntax: + +```yaml +greaterOrEquals: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleGreaterOrEquals' +spec: + if: + field: 'Name' + greaterOrEquals: 3 +``` + +### HasValue + +The `hasValue` condition determines if the field exists and has a non-empty value. + +Syntax: + +```yaml +hasValue: +``` + +- When `hasValue: true`, hasValue will return `true` if the field is not empty. +- When `hasValue: false`, hasValue will return `true` if the field is empty. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleHasValue' +spec: + if: + field: 'Name' + hasValue: true +``` + +### In + +The `in` condition can be used to compare if a field contains one of the specified values. + +Syntax: + +```yaml +in: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIn' +spec: + if: + field: 'Name' + in: + - 'Value1' + - 'Value2' +``` + +### IsLower + +The `isLower` condition determines if the operand is a lowercase string. + +Syntax: + +```yaml +isLower: +``` + +- When `isLower: true`, _isLower_ will return `true` if the operand is a lowercase string. + Non-letter characters are ignored. +- When `isLower: false`, _isLower_ will return `true` if the operand is not a lowercase string. +- If the operand is a field, and the field does not exist _isLower_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsLower' +spec: + if: + field: 'Name' + isLower: true +``` + +### IsString + +The `isString` condition determines if the operand is a string or other type. + +Syntax: + +```yaml +isString: +``` + +- When `isString: true`, _isString_ will return `true` if the operand is a string. +- When `isString: false`, _isString_ will return `true` if the operand is not a string or is null. +- If the operand is a field, and the field does not exist _isString_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsString' +spec: + if: + field: 'Name' + isString: true +``` + +### IsUpper + +The `isUpper` condition determines if the operand is an uppercase string. + +Syntax: + +```yaml +isUpper: +``` + +- When `isUpper: true`, _isUpper_ will return `true` if the operand is an uppercase string. + Non-letter characters are ignored. +- When `isUpper: false`, _isUpper_ will return `true` if the operand is not an uppercase string. +- If the operand is a field, and the field does not exist _isUpper_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleIsUpper' +spec: + if: + field: 'Name' + isUpper: true +``` + +### Less + +Syntax: + +```yaml +less: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleLess' +spec: + if: + field: 'Name' + less: 3 +``` + +### LessOrEquals + +Syntax: + +```yaml +lessOrEquals: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleLessOrEquals' +spec: + if: + field: 'Name' + lessOrEquals: 3 +``` + +### Match + +The `match` condition can be used to compare if a field matches a supplied regular expression. + +Syntax: + +```yaml +match: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleMatch' +spec: + if: + field: 'Name' + match: '$(abc|efg)$' +``` + +### Not + +The `any` operator is used to invert the result of the nested expression. +When a nested expression matches, `not` does not match. +When a nested expression does not match, `not` matches. + +Syntax: + +```yaml +not: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleNot' +spec: + if: + not: + # The AlternativeName field must not exist. + field: 'AlternativeName' + exists: true +``` + +### NotEquals + +The `notEquals` condition can be used to compare if a field is equal to a supplied value. + +Syntax: + +```yaml +notEquals: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleNotEquals' +spec: + if: + field: 'Name' + notEquals: 'TargetObject1' +``` + +### NotIn + +The `notIn` condition can be used to compare if a field does not contains one of the specified values. + +Syntax: + +```yaml +notIn: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleNotIn' +spec: + if: + field: 'Name' + notIn: + - 'Value1' + - 'Value2' +``` + +### NotMatch + +The `notMatch` condition can be used to compare if a field does not matches a supplied regular expression. + +Syntax: + +```yaml +notMatch: +``` + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleNotMatch' +spec: + if: + field: 'Name' + notMatch: '$(abc|efg)$' +``` + +### StartsWith + +The `startsWith` condition can be used to determine if the operand starts with a specified string. +One or more strings to compare can be specified. + +Syntax: + +```yaml +startsWith: +``` + +- If the operand is a field, and the field does not exist, _startsWith_ always returns `false`. + +For example: + +```yaml +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: 'ExampleStartsWith' +spec: + if: + anyOf: + - field: 'url' + startsWith: 'http' + - field: 'url' + startsWith: + - 'http://' + - 'https://' +``` + +## EXAMPLES + +### Example Selectors.Doc.yaml + +```yaml +# Example Selectors.Doc.yaml +--- +# Synopsis: Require the CustomValue field. +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: RequireCustomValue +spec: + if: + field: 'CustomValue' + exists: true + +--- +# Synopsis: Require a Name or AlternativeName. +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: RequireName +spec: + if: + anyOf: + - field: 'AlternateName' + exists: true + - field: 'Name' + exists: true + +--- +# Synopsis: Require a specific CustomValue +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: RequireSpecificCustomValue +spec: + if: + field: 'CustomValue' + in: + - 'Value1' + - 'Value2' +``` + +## NOTE + +An online version of this document is available at https://microsoft.github.io/PSDocs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md. + +## SEE ALSO + +- [Invoke-PSDocs](https://microsoft.github.io/PSDocs/commands/PSDocs/en-US/Invoke-PSDocs.html) + +## KEYWORDS + +- Selectors +- PSDocs diff --git a/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md new file mode 100644 index 00000000..c0c0e31f --- /dev/null +++ b/packages/psdocs/docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md @@ -0,0 +1,247 @@ +# PSDocs_Variables + +## about_PSDocs_Variables + +## SHORT DESCRIPTION + +Describes the automatic variables that can be used within PSDocs document definitions. + +## LONG DESCRIPTION + +PSDocs lets you generate dynamic markdown documents using PowerShell blocks. +To generate markdown, a document is defined inline or within script files by using the `document` keyword. + +Within a document definition, PSDocs exposes several automatic variables that can be read to assist with dynamic document generation. +Overwriting these variables or variable properties is not supported. + +The following variables are available for use: + +- [$Culture](#culture) +- [$Document](#document) +- [$InstanceName](#instancename) +- [$LocalizedData](#localizeddata) +- [$PSDocs](#psdocs) +- [$TargetObject](#targetobject) +- [$Section](#section) + +### Culture + +The name of the culture currently being processed. +`$Culture` is set by using the `-Culture` parameter of `Invoke-PSDocument` or inline functions. + +When more than one culture is set, each will be processed sequentially. +If a culture has not been specified, `$Culture` will default to the culture of the current thread. + +Syntax: + +```powershell +$Culture +``` + +### Document + +An object representing the current object model of the document during generation. + +The following section properties are available for public read access: + +- `Title` - The title of the document. +- `Metadata` - A dictionary of metadata key/value pairs. +- `Path` - The file path where the document will be written to. + +Syntax: + +```powershell +$Document +``` + +Examples: + +```powershell +document 'Sample' { + Title 'Example' + + # The value of $Document.Title = 'Example' + "The title of the document is $($Document.Title)." + + Metadata @{ + author = 'Bernie' + } + + # The value of $Document.Metadata['author'] = 'Bernie' + 'The author is ' + $Document.Metadata['author'] + '.' +} +``` + +```text +--- +author: Bernie +--- +# Example +The title of the document is Example. +The author is Bernie. +``` + +### InstanceName + +The name of the instance currently being processed. +`$InstanceName` is set by using the `-InstanceName` parameter of `Invoke-PSDocument` or inline functions. + +When more than one instance name is set, each will be processed sequentially. +If an instance name is not specified, `$InstanceName` will default to the name of the document definition. + +Syntax: + +```powershell +$InstanceName +``` + +### LocalizedData + +A dynamic object with properties names that map to localized strings for the current culture. +Localized strings are read from a `PSDocs-strings.psd1` file within a culture subdirectory. +When the `.Doc.ps1` is loose, the culture subdirectory is within the same directory as the `.Doc.ps1`. +If the `.Doc.ps1` is shipped in a module the culture subdirectory is relative to the module manifest _.psd1_ file. + +When accessing localized data: + +- String names are case sensitive. +- String values are read only. + +Syntax: + +```powershell +$LocalizedData. +``` + +Examples: + +```powershell +# Data for strings stored in PSDocs-strings.psd1 +@{ + WithLocalizedString = 'Localized string for en-ZZ. Format={0}.' +} +``` + +```powershell +# Synopsis: Use -f to generate a formatted localized string +Document 'WithLocalizedData' { + $LocalizedData.WithLocalizedString -f $TargetObject.Type; +} +``` + +This document returns content similar to: + +```text +Localized string for en-ZZ. Format=TestType. +``` + +### PSDocs + +An object representing the current context of PSDocs. + +In addition, `$PSDocs` provides several helper properties and functions. + +The following properties are available for public read access: + +- `Configuration` - An object with custom configuration properties. +Each configuration key specified in `ps-docs.yaml` is assessable as a property. +Additionally helper methods can be used. +See `about_PSDocs_Configuration` for more information. +- `Culture` - The name of the culture currently being processed. +- `Document` - A document context object. +- `Output` - All the document results generated. +This property is only available within `End` convention blocks. +- `TargetObject` - The value of the pipeline object currently being processed. + +Syntax: + +```powershell +$PSDocs +``` + +```powershell +# Get the value of the custom configuration 'Key1'. +$PSDocs.Configuration.Key1 +``` + +```powershell +# Return the currently processed culture. e.g. 'en-US' +$PSDocs.Culture +``` + +```powershell +# Access document context properties. +$PSDocs.Document.InstanceName +$PSDocs.Document.OutputPath +``` + +```powershell +# Return the current pipeline object. +$PSDocs.TargetObject +``` + +### TargetObject + +The value of the pipeline object currently being processed. +`$TargetObject` is set by using the `-InputObject` parameter of `Invoke-PSDocument` or inline functions. + +When more than one input object is set, each object will be processed sequentially. +If an input object is not specified, `$TargetObject` will default to `$Null`. + +Syntax: + +```powershell +$TargetObject +``` + +### Section + +An object of the document section currently being processed. + +As `Section` blocks are processed, the `$Section` variable will be updated to match the block that is currently being processed. +`$Section` will be the current document outside of `Section` blocks. + +The following section properties are available for public read access: + +- `Title` - The title of the section, or the document (when outside of a section block). +- `Level` - The section heading depth. This will be _2_ (or greater for nested sections), or _1_ (when outside of a section block). + +Syntax: + +```powershell +$Section +``` + +Examples: + +```powershell +document 'Sample' { + Section 'Introduction' { + # The value of $Section.Title = 'Introduction' + "The current title is $($Section.Title)." + } +} +``` + +```text +## Introduction + +The current section title is Introduction. +``` + +## NOTE + +An online version of this document is available at https://github.com/Microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Variables.md. + +## SEE ALSO + +- [Invoke-PSDocument](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md) + +## KEYWORDS + +- Culture +- Document +- InstanceName +- PSDocs +- TargetObject +- Section diff --git a/packages/psdocs/docs/examples/Get-child-item-output.md b/packages/psdocs/docs/examples/Get-child-item-output.md new file mode 100644 index 00000000..1202bffd --- /dev/null +++ b/packages/psdocs/docs/examples/Get-child-item-output.md @@ -0,0 +1,11 @@ + +## Introduction +This is a sample file list from C:\\ + +|Name|PSIsContainer| +| --- | --- | +|PerfLogs|True| +|Program Files|True| +|Program Files (x86)|True| +|Users|True| +|Windows|True| \ No newline at end of file diff --git a/packages/psdocs/docs/examples/SharePoint-config-output.md b/packages/psdocs/docs/examples/SharePoint-config-output.md new file mode 100644 index 00000000..d90453bf --- /dev/null +++ b/packages/psdocs/docs/examples/SharePoint-config-output.md @@ -0,0 +1,44 @@ +# Server1 + +## Installation + +|InstallerPath|OnlineMode| +| --- | --- | +|C:\\binaries\\prerequisiteinstaller.exe|True| + +|BinaryDir| +| --- | +|C:\\binaries\\| + +## Farm + +|DatabaseServer|FarmConfigDatabaseName|AdminContentDatabaseName| +| --- | --- | --- | +|sql.contoso.com|SP_Config|SP_AdminContent| + +## Installed services + +|Name|Ensure| +| --- | --- | +|Claims to Windows Token Service|Present| +|Secure Store Service|Present| +|SharePoint Server Search|Present| + +## Site +See the site configuration below. + +|Url|OwnerAlias|Name|Template| +| --- | --- | --- | --- | +|http://sites.contoso.com|CONTOSO\\SP_Admin|DSC Demo Site|STS#0| + +### Web applications + +|Name|Url|Port|HostHeader|ApplicationPool|AuthenticationMethod|AllowAnonymous| +| --- | --- | --- | --- | --- | --- | --- | +|SharePoint Sites|http://sites.contoso.com|80|sites.contoso.com|SharePoint Sites|NTLM|False| + +## Logging + +|LogPath|DaysToKeepLogs|LogCutInterval| +| --- | --- | --- | +|C:\\ULS|7|15| diff --git a/packages/psdocs/docs/examples/Yaml-header-output.md b/packages/psdocs/docs/examples/Yaml-header-output.md new file mode 100644 index 00000000..c03d9482 --- /dev/null +++ b/packages/psdocs/docs/examples/Yaml-header-output.md @@ -0,0 +1,6 @@ +--- +title: An example title +author: bewhite +last-updated: 2018-05-17 +--- +Yaml header may not be rendered by some markdown viewers. See source to view yaml. diff --git a/packages/psdocs/docs/install-instructions.md b/packages/psdocs/docs/install-instructions.md new file mode 100644 index 00000000..594fa0ae --- /dev/null +++ b/packages/psdocs/docs/install-instructions.md @@ -0,0 +1,80 @@ +# Install instructions + +## Prerequisites + +- Windows PowerShell 5.1 with .NET Framework 4.7.2+ or +- PowerShell Core 6.2 or greater on Windows, MacOS and Linux or +- PowerShell 7.0 or greater on Windows, MacOS and Linux + +For a list of platforms that PowerShell 7.0 is supported on [see][get-powershell]. + +## Getting the module + +Install from [PowerShell Gallery][module] for all users (requires permissions): + +```powershell +# Install PSDocs module +Install-Module -Name 'PSDocs' -Repository PSGallery; +``` + +Install from [PowerShell Gallery][module] for current user only: + +```powershell +# Install PSDocs module +Install-Module -Name 'PSDocs' -Repository PSGallery -Scope CurrentUser; +``` + +Save for offline use from PowerShell Gallery: + +```powershell +# Save PSDocs module, in the .\modules directory +Save-Module -Name 'PSDocs' -Repository PSGallery -Path '.\modules'; +``` + +> For pre-release versions the `-AllowPrerelease` switch must be added when calling `Install-Module` or `Save-Module`. +> +> To install pre-release module versions, upgrading to the latest version of _PowerShellGet_ may be required. +To do this use: +> +> `Install-Module -Name PowerShellGet -Repository PSGallery -Scope CurrentUser -Force` + +## Building from source + +To build this module from source run `./build.ps1`. +This build script will compile the module and documentation then output the result into `out/modules/PSDocs`. + +The following PowerShell modules will be automatically downloaded if the required versions are not present: + +- PlatyPS +- Pester +- PSScriptAnalyzer +- PowerShellGet +- PackageManagement +- InvokeBuild + +These additional modules are only required for building PSDocs and are not required for running PSDocs. + +If you are on a network that does not permit Internet access to the PowerShell Gallery, +download these modules on an alternative device that has access. +The following script can be used to download the required modules to an alternative device. +After downloading the modules copy the module directories to devices with restricted Internet access. + +```powershell +# Save modules, in the .\modules directory +Save-Module -Name PlatyPS, Pester, PSScriptAnalyzer, PowerShellGet, PackageManagement, InvokeBuild -Repository PSGallery -Path '.\modules'; +``` + +Additionally .NET Core SDK v3.1 is required. +.NET Core will not be automatically downloaded and installed. +To download and install the latest SDK see [Download .NET Core 3.1][dotnet]. + +## Upgrading from previous versions + +If you are upgrading PSDocs from a previous version: + +- Please review any breaking changes in the [change log](../CHANGELOG.md). +- See [upgrade notes](upgrade-notes.md) for helpful information. + +[module]: https://www.powershellgallery.com/packages/PSDocs +[get-powershell]: https://github.com/PowerShell/PowerShell#get-powershell +[dotnet]: https://dotnet.microsoft.com/download/dotnet-core/3.1 diff --git a/packages/psdocs/docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md b/packages/psdocs/docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md new file mode 100644 index 00000000..b9eb0c35 --- /dev/null +++ b/packages/psdocs/docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md @@ -0,0 +1,634 @@ +# PSDocs_Keywords + +## about_PSDocs_Keywords + +## SHORT DESCRIPTION + +Describes the language keywords that can be used within PSDocs document definitions. + +## LONG DESCRIPTION + +PSDocs lets you generate dynamic markdown documents using PowerShell blocks. +To generate markdown, a document is defined within script files by using the `document` keyword. + +Within a document definition, PSDocs keywords in addition to regular PowerShell expressions can be used to dynamically generate documents. + +The following PSDocs keywords are available: + +- [Document](#document) - A named document definition +- [Section](#section) - A named section +- [Title](#title) - Sets the document title +- [Code](#code) - Inserts a block of code +- [BlockQuote](#blockquote) - Inserts a block quote +- [Note](#note) - Inserts a note using DocFx formatted markdown (DFM) +- [Warning](#warning) - Inserts a warning using DocFx formatted markdown (DFM) +- [Table](#table) - Inserts a table from pipeline objects +- [Metadata](#metadata) - Inserts a YAML header +- [Include](#include) - Insert content from an external file + +### Document + +Defines a named block that can be called to output documentation. +The document keyword can be defined in a script file ending with the `.Doc.ps1` extension. + +Syntax: + +```text +Document [-Name] [-Tag ] [-With ] [-If ] [-Body] { + ... +} +``` + +- `Name` - The name of the document definition. +- `Tag` - A hashtable of key/ value metadata that can be used to filter specific definitions. +- `With` - A selector pre-condition that must evaluate true before the document definition is executed. +- `If` - A script pre-condition that must evaluate to `$True` before the document definition is executed. +- `Body` - A definition of the markdown document containing one or more PSDocs keywords and PowerShell. + +Each document definition is top level, and can't be nested within each other. + +Examples: + +```powershell +# Example: .\Sample.Doc.ps1 + +# A document definition named Sample +Document 'Sample' { + + Section 'Introduction' { + 'This is a sample document that uses PSDocs keywords to construct a dynamic document.' + } + + Section 'Generated by' { + "This document was generated by $($Env:USERNAME)." + + $PSVersionTable | Table -Property Name,Value + } +} +``` + +Invoking the document definition: + +```powershell +# Will call all document definitions in files with the .Doc.ps1 extension within the current working path +Invoke-PSDocument; +``` + +```powershell +# Call a specific document definition by name, from a specific file +Invoke-PSDocument -Name 'Sample' -Path '.\Sample.Doc.ps1'; +``` + +### Section + +Creates a new section block containing content. Each section will be converted into a header. +Section headers start at level 2 (i.e. `##`), and can be nested to create subsections with level 3, 4, 5 headers. + +By default, if a section is empty it is skipped. + +Syntax: + +```text +Section [-Name] [-If ] [-Force] [-Body] +``` + +- `Name` - The name or header of the section. +- `If` - A condition to determine if the section block should be included in the markdown document. +- `Force` - Force the creation of the section even if the section has no content. + +Examples: + +```powershell +# A document definition named Sample +Document 'Sample' { + + # Define a section named Introduction + Section 'Introduction' { + + # Content of the Introduction section + 'This is a sample document that uses PSDocs keywords to construct a dynamic document.' + + # Define more section content here + } +} +``` + +```text +## Introduction +This is a sample document that uses PSDocs keywords to construct a dynamic document. +``` + +```powershell +# A document definition named Sample +Document 'Sample' { + + # Sections can be nested + Section 'Level2' { + + Section 'Level3' -Force { + + # Define level 3 section content here + } + + # Define more level 2 section content here + } +} +``` + +```text +## Level2 +### Level3 +``` + +```powershell +# A document definition named Sample +Document 'Sample' { + + # By default each section is included when markdown in generated + Section 'Included in output' -Force { + + # Section and section content is included in generated markdown + } + + # Sections can be optional if the If parameter is specified the expression evaluates to $False + Section 'Not included in output' -If { $False } { + + # Section and section content is not included in generated markdown + 'Content that is not included' + } +} +``` + +```text +## Included in output +``` + +### Title + +You can use the Title statement to set the title of the document. + +Syntax: + +```text +Title [-Content] +``` + +- `Content` - Set the title for the document. + +Examples: + +```powershell +# A document definition named Sample +Document 'Sample' { + + # Set the title for the document + Title 'Level 1' + + Section 'Level 2' -Force { + + } +} +``` + +```text +# Level 1 +## Level 2 +``` + +Generates a new `Sample.md` document containing the heading `Level 1`. + +### Code + +You can use the Code statement to generate fenced code sections in markdown. +An info string can optionally be specified using the `-Info` parameter. + +Syntax with block: + +```text +Code [-Info] [] [-Body] +``` + +Syntax with pipeline: + +```text + | Code [-Info] [] +``` + +- `Info` - An info string that can be used to specify the language of the code block. +By default, no info string will be set unless a script block is used. +When a script block is used, the info string will default to `powershell` unless overridden. + +When the following info strings are used, PSDocs will automatic serialize custom objects. + +- `json` - Serializes as JSON. +- `yaml`, or `yml` - Serializes as YAML. + +Examples: + +```powershell +# A document definition named CodeBlock +Document 'CodeBlock' { + + # Define a code block that will be rendered as markdown instead of being executed + Code { + powershell.exe -Help + } +} +``` + +Generates a new `CodeBlock.md` document containing the `powershell.exe -Help` command line. + + ```powershell + powershell.exe -Help + ``` + +```powershell +# A document definition named CodeBlockWithInfo +Document 'CodeBlockWithInfo' { + + # Define a code block that will be rendered in markdown as PowerShell + Code powershell { + Get-Item -Path .\; + } +} +``` + +Generates a new document containing script code formatted with the `powershell` info string. + + ```powershell + Get-Item -Path .\; + ``` + +```powershell +# A document definition named CodeBlockFromPipeline +Document 'CodeBlockFromPipeline' { + + # Execute Get-Help then create a code block from the output of the Get-Help command + Get-Help 'Invoke-PSDocument' | Code +} +``` + +Generates a new document from the output of `Get-Help`. + +```powershell +Document 'CodeYaml' { + [PSCustomObject]@{ + Name = 'Value' + } | Code 'yaml' +} +``` + +Generates a new document with a YAML code block. + + ```yaml + Name: Value + ``` + +### BlockQuote + +Creates a block quote formatted section. + +Syntax: + +```text +BlockQuote [-Text] [-Title ] [-Info ] +``` + +- `Text` - The text of the block quote. This parameter can be specified directly or accept input from the pipeline. +- `Title` - An additional title to add to the top of the text provided by the `-Text` parameter. +- `Info` - The type of block quote. By default PSDocs will format using a DocFX Formatted Markdown (DFM) syntax. + +Examples: + +```powershell +# A document definition named BlockQuote +Document 'BlockQuote' { + + # Block quote some text + 'This is a block quote.' | BlockQuote +} +``` + +Generates a new `BlockQuote.md` document containing a block quote. + +```text +> This is a block quote. +``` + +```powershell +# A document definition named BlockQuote +Document 'BlockQuote' { + + # Block quote some text + 'This is a block quote.' | BlockQuote -Title 'NB:' +} +``` + +```text +> NB: +> This is a block quote. +``` + +```powershell +# A document definition named BlockQuote +Document 'BlockQuote' { + + # Block quote some text + 'This is a block quote.' | BlockQuote -Info 'Note' +} +``` + +```text +> [!NOTE] +> This is a block quote. +``` + +### Note + +Creates a block quote formatted as a DocFx Formatted Markdown (DFM) note. +This is an alternative to using the `BlockQuote` keyword. + +Syntax: + +```text +Note -Text +``` + +- `Text` - The text of the note. +This parameter can be specified directly or accept input from the pipeline. + +Examples: + +```powershell +# A document definition named NoteBlock +Document 'NoteBlock' { + + # Define a note block + 'This is a note.' | Note +} +``` + +```text +> [!NOTE] +> This is a note. +``` + +Generates a new `NoteBlock.md` document containing a block quote formatted as a DFM note. + +### Warning + +Creates a block quote formatted as a DocFx Formatted Markdown (DFM) warning. +This is an alternative to using the `BlockQuote` keyword. + +Syntax: + +```text +Warning -Text +``` + +- `Text` - The text of the warning. +This parameter can be specified directly or accept input from the pipeline. + +Examples: + +```powershell +# A document definition named WarningBlock +Document 'WarningBlock' { + + 'This is a warning.' | Warning +} +``` + +```text +> [!WARNING] +> This is a warning. +``` + +Generates a new `WarningBlock.md` document containing a block quote formatted as a DFM warning. + +### Table + +Creates a formatted table from pipeline objects. + +Syntax: + +```text +Table [-Property ] +``` + +- `Property` - Filter the table to only the named columns. Either a named column or hash table can be used. Valid keys include: + - `Name` (or `Label`) `[String]` - the name of the column + - `Expression` `[String]` or `[ScriptBlock]` - an expression to get a calculated column value + - `Width` `[Int32]` - columns will be padded with spaces in markdown to this width. This doesn't change how the the table is rendered + - `Alignment` (value can be "Left", "Center" or "Right") - alignment to align column values in during rendering + +Examples: + +```powershell +# A document definition named SimpleTable +Document 'SimpleTable' { + + Section 'Directory list' { + + # Create a row for each child item of C:\ + Get-ChildItem -Path 'C:\' | Table -Property Name,PSIsContainer; + } +} +``` + +```text +## Directory list + +Name | PSIsContainer +---- | ------------- +Program Files | True +Program Files (x86) | True +Users | True +Windows | True +``` + +Generates a new `SimpleTable.md` document containing a table populated with a row for each item. +Only the properties Name and PSIsContainer are added as columns. + +```powershell +# A document definition named CalculatedTable +Document 'CalculatedTable' { + + Section 'Directory list' { + + # Create a row for each child item of C:\ + Get-ChildItem -Path 'C:\' | Table -Property @{ Name = 'Name'; Expression = { $_.Name }; Width = 19; },@{ Name = 'Is Container'; Expression = { $_.PSIsContainer }; Alignment = 'Center'; Width = 11; }; + } +} +``` + +```text +## Directory list + +Name | Is Container +---- | :----------: +Program Files | True +Program Files (x86) | True +Users | True +Windows | True +``` + +Generates a new `CalculatedTable.md` document containing a table populated with a row for each item. +Only the properties Name and Is Container are added as columns. +A property expression is used on the `PSIsContainer` property to render the column as `Is Container`. + +### Metadata + +Creates a metadata header, that will be rendered as yaml front matter. +Multiple `Metadata` blocks can be used and they will be aggregated together. + +Syntax: + +```text +Metadata [-Body] +``` + +Examples: + +```powershell +# A document definition named MetadataBlock +Document 'MetadataBlock' { + + # Create a Metadata block of key value pairs + Metadata @{ + title = 'An example title' + } + + Metadata @{ + author = $Env:USERNAME + 'last-updated' = (Get-Date).ToString('yyyy-MM-dd') + } + + # Additional text to add to the document + 'Yaml header may not be rendered by some markdown viewers. See source to view yaml.' +} + +# Generate markdown from the document definition +MetadataBlock; +``` + +Generates a new MetadataBlock.md document containing a yaml front matter. +An example of the output generated is available [here](/docs/examples/Yaml-header-output.md). + +```text +--- +title: An example title +author: bewhite +last-updated: 2018-05-17 +--- +Yaml header may not be rendered by some markdown viewers. See source to view yaml. +``` + +### Include + +Insert content from an external file into this document. + +Syntax: + +```text +Include [-FileName] [-BaseDirectory ] [-UseCulture] [-Replace ] +``` + +- `FileName` - The path to a markdown file to include. An absolute or relative path is accepted. +- `BaseDirectory` - The base path to work from for relative paths specified with the `FileName` parameter. +By default this is the current working path. +- `UseCulture` - When specified include will look for the file within a subdirectory for a named culture. +- `Replace` - When specified include will replace keys occurring in the file with the specified value. +Replacement keys are case-sensitive. + +Examples: + +```powershell +# A document definition +Document 'Sample' { + # Include IncludeFile.md from the current working path + Include IncludeFile.md +} +``` + +```text +This is included from an external file. +``` + +```powershell +# A document definition +Document 'Sample' { + # Include IncludeFile.md from docs/ + Include IncludeFile.md -BaseDirectory docs +} +``` + +```text +This is included from an external file. +``` + +```powershell +# A document definition +Document 'Sample' { + # Include IncludeFile.md from docs//. i.e. docs/en-AU/ + Include IncludeFile.md -BaseDirectory docs -UseCulture +} +``` + +```text +This is included from an external file. +``` + +```powershell +# A document definition +Document 'Sample' { + # Include IncludeFile.md replacing 'included' with 'an example' + Include IncludeFile.md -Replace @{ + 'included' = 'an example' + } +} +``` + +```text +This is an example from an external file. +``` + +## EXAMPLES + +```powershell + +Document 'Sample' { + + Section 'Introduction' { + 'This is a sample document that uses PSDocs keywords to construct a dynamic document.' + } + + Section 'Generated by' { + "This document was generated by $($Env:USERNAME)." + + $PSVersionTable | Table -Property Name,Value + } +} +``` + +## NOTE + +An online version of this document is available at https://github.com/Microsoft/PSDocs/blob/main/docs/keywords/PSDocs/en-US/about_PSDocs_Keywords.md. + +## SEE ALSO + +- [Invoke-PSDocument](https://github.com/Microsoft/PSDocs/blob/main/docs/commands/PSDocs/en-US/Invoke-PSDocument.md) + +## KEYWORDS + +- Document +- Section +- Title +- Code +- BlockQuote +- Note +- Warning +- Table +- Metadata +- Yaml +- Include diff --git a/packages/psdocs/docs/scenarios/arm-template/arm-template.Doc.ps1 b/packages/psdocs/docs/scenarios/arm-template/arm-template.Doc.ps1 new file mode 100644 index 00000000..cf76c044 --- /dev/null +++ b/packages/psdocs/docs/scenarios/arm-template/arm-template.Doc.ps1 @@ -0,0 +1,67 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Azure Resource Manager documentation definitions +# + +# A function to break out parameters from an ARM template +function global:GetTemplateParameter { + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + $template = Get-Content $Path | ConvertFrom-Json; + foreach ($property in $template.parameters.PSObject.Properties) { + [PSCustomObject]@{ + Name = $property.Name + Description = $property.Value.metadata.description + } + } + } +} + +# A function to import metadata +function global:GetTemplateMetadata { + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + $metadata = Get-Content $Path | ConvertFrom-Json; + return $metadata; + } +} + +# Synopsis: A definition to generate markdown for an ARM template +document 'arm-template' { + + # Read JSON files + $metadata = GetTemplateMetadata -Path $PSScriptRoot/metadata.json; + $parameters = GetTemplateParameter -Path $PSScriptRoot/template.json; + + # Set document title + Title $metadata.itemDisplayName + + # Write opening line + $metadata.Description + + # Add each parameter to a table + Section 'Parameters' { + $parameters | Table -Property @{ Name = 'Parameter name'; Expression = { $_.Name }},Description + } + + # Generate example command line + Section 'Use the template' { + Section 'PowerShell' { + 'New-AzResourceGroupDeployment -Name -ResourceGroupName -TemplateFile ' | Code powershell + } + + Section 'Azure CLI' { + 'az group deployment create --name --resource-group --template-file ' | Code text + } + } +} diff --git a/packages/psdocs/docs/scenarios/arm-template/arm-template.md b/packages/psdocs/docs/scenarios/arm-template/arm-template.md new file mode 100644 index 00000000..50dfabe1 --- /dev/null +++ b/packages/psdocs/docs/scenarios/arm-template/arm-template.md @@ -0,0 +1,166 @@ +# Azure Resource Manager template example + +This is an example of how PSDocs can be used to generate documentation for an ARM template. +Documentation for ARM templates might be used by an internal technical team, who creates and maintains ARM templates for their company. + +In this scenario we will use two JSON files: + +- `template.json` - A valid ARM template +- `metadata.json` - Contains information about the template that isn't part of the ARM template specification + +> Our `metadata.json` uses the same schema used in the Azure Quick Start Templates GitHub repository. + +When deploying an ARM template, knowing what parameters are available and how they can be used is important, so this will be a key part of our documentation. + +Fortunately, the ARM template specification allows for metadata per parameter, and a common use for this is to define a parameter description. + +An example parameter might look like this: + +```json +"environment": { + "type": "string", + "metadata": { + "description": "The environment that the resource will be deployed to. Either production or internal." + } +} +``` + +## Define helper functions + +We will need to import our two JSON files and convert them to objects so that we can easily read the name of each parameter, but also the description. + +While this could be done inline, we will create separate functions that can be called as required. +Using separate functions in this case will improve the readability of our code. + +```powershell +# A function to break out parameters from an ARM template +function global:GetTemplateParameter { + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + process { + $template = Get-Content $Path | ConvertFrom-Json; + foreach ($property in $template.parameters.PSObject.Properties) { + [PSCustomObject]@{ + Name = $property.Name + Description = $property.Value.metadata.description + } + } + } +} + +# A function to import metadata +function global:GetTemplateMetadata { + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + process { + $metadata = Get-Content $Path | ConvertFrom-Json; + return $metadata; + } +} +``` + +## Create a document definition + +PSDocs uses the `Document` keyword to describe a document definition. +A document definition is designed to be reusable. + +With our two helper functions already implemented, we are ready to define our document. +For our example, our JSON files are in the same directory as the documentation definition so we are using `$PSScriptRoot`. + +```powershell +Document 'arm-template' { + + # Read JSON files + $metadata = GetTemplateMetadata -Path $PSScriptRoot/metadata.json; + $parameters = GetTemplateParameter -Path $PSScriptRoot/template.json; +} +``` + +We want to set a title and an opening description for our document based on the metadata file. + +```powershell +Document 'arm-template' { + + # Read JSON files + $metadata = GetTemplateMetadata -Path $PSScriptRoot/metadata.json; + $parameters = GetTemplateParameter -Path $PSScriptRoot/template.json; + + # Set document title + Title $metadata.itemDisplayName + + # Write opening line + $metadata.Description +} +``` + +Next we need to output the template parameters into a table with metadata descriptions. +To format our parameters in a table we use the `Table` keyword. + +```powershell +Document 'arm-template' { + + # Read JSON files + $metadata = GetTemplateMetadata -Path $PSScriptRoot/metadata.json; + $parameters = GetTemplateParameter -Path $PSScriptRoot/template.json; + + ... + + # Add each parameter to a table + Section 'Parameters' { + $parameters | Table -Property @{ Name = 'Parameter name'; Expression = { $_.Name }},Description + } +} +``` + +We can also provide an example command line that can be used to deploy our ARM template. +To insert a code sample use the `Code` keyword. + +```powershell +Document 'arm-template' { + + ... + + # Generate example command line + Section 'Use the template' { + Section 'PowerShell' { + 'New-AzResourceGroupDeployment -Name -ResourceGroupName -TemplateFile -TemplateParameterFile ' | Code powershell + } + + Section 'Azure CLI' { + 'az group deployment create --name --resource-group --template-file --parameters @' | Code text + } + } +} +``` + +## Generate markdown + +Document definitions can be called inline or from a path. +In this example, we've saved our definition to a file. + +To generate markdown from a path, we used the `Invoke-PSDocument` cmdlet with the `-Path` parameter. + +Examples: + +```powershell +# Find and build any document definitions in the currently working path (and subdirectories) +Invoke-PSDocument -Path .; +``` + +In this case, we are generating documentation with the definition and output saved in this repository so we use the`-OutputPath` and `-InstanceName` parameters. + +```powershell +# Generate docs/scenarios/arm-template/output.md +Invoke-PSDocument -Path '.\docs\scenarios\arm-template' -OutputPath '.\docs\scenarios\arm-template\' -InstanceName 'output'; +``` + +## More information + +- [Get the full script](arm-template.Doc.ps1) +- [Example ARM template file](template.json) +- [Example ARM metadata file](metadata.json) +- [Example output](output.md) diff --git a/packages/psdocs/docs/scenarios/arm-template/metadata.json b/packages/psdocs/docs/scenarios/arm-template/metadata.json new file mode 100644 index 00000000..8a16fa3b --- /dev/null +++ b/packages/psdocs/docs/scenarios/arm-template/metadata.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://aka.ms/azure-quickstart-templates-metadata-schema#", + "type": "QuickStart", + "itemDisplayName": "AKS deployment with a virtual network", + "description": "This template creates a AKS cluster using a virtual network in the same resource group", + "summary": "Creates and deploys a Kubernetes cluster in a virtual network.", + "githubUsername": "BernieWhite", + "dateUpdated": "2018-10-10" +} diff --git a/packages/psdocs/docs/scenarios/arm-template/output.md b/packages/psdocs/docs/scenarios/arm-template/output.md new file mode 100644 index 00000000..db71ce86 --- /dev/null +++ b/packages/psdocs/docs/scenarios/arm-template/output.md @@ -0,0 +1,44 @@ +# AKS deployment with a virtual network + +This template creates a AKS cluster using a virtual network in the same resource group + +## Parameters + +Parameter name | Description +-------------- | ----------- +resourceName | The name of the Managed Cluster resource. +environment | The environment that the resource will be deployed to. Either production or internal. +location | The location of AKS resource. +dnsPrefix | Optional DNS prefix to use with hosted Kubernetes API server FQDN. +agentCount | The number of agent nodes for the cluster. +agentVMSize | The size of the Virtual Machine. +servicePrincipalClientId | Client ID (used by cloudprovider) +servicePrincipalClientSecret | The Service Principal Client Secret. +osType | The type of operating system. +kubernetesVersion | The version of Kubernetes. +enableOmsAgent | boolean flag to turn on and off of omsagent addon +workspaceRegion | Specify the region for your OMS workspace +workspaceName | Specify the name of the OMS workspace +omsWorkspaceId | Specify the resource id of the OMS workspace +omsSku | Select the SKU for your workspace +enableHttpApplicationRouting | boolean flag to turn on and off of http application routing +networkPlugin | Network plugin used for building Kubernetes network. +maxPods | Maximum number of pods that can run on a node. +vnetSubnetID | Resource ID of virtual network subnet used for nodes and/or pods IP assignment. +serviceCidr | A CIDR notation IP range from which to assign service cluster IPs. +dnsServiceIP | Containers DNS server IP address. +dockerBridgeCidr | A CIDR notation IP for Docker bridge. + +## Use the template + +### PowerShell + +```powershell +New-AzResourceGroupDeployment -Name -ResourceGroupName -TemplateFile +``` + +### Azure CLI + +```text +az group deployment create --name --resource-group --template-file +``` diff --git a/packages/psdocs/docs/scenarios/arm-template/template.json b/packages/psdocs/docs/scenarios/arm-template/template.json new file mode 100644 index 00000000..8676cd5c --- /dev/null +++ b/packages/psdocs/docs/scenarios/arm-template/template.json @@ -0,0 +1,286 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceName": { + "type": "string", + "metadata": { + "description": "The name of the Managed Cluster resource." + } + }, + "environment": { + "type": "string", + "defaultValue": "internal", + "allowedValues": [ + "production", + "internal" + ], + "metadata": { + "description": "The environment that the resource will be deployed to. Either production or internal." + } + }, + "location": { + "type": "string", + "metadata": { + "description": "The location of AKS resource." + } + }, + "dnsPrefix": { + "type": "string", + "metadata": { + "description": "Optional DNS prefix to use with hosted Kubernetes API server FQDN." + } + }, + "agentCount": { + "type": "int", + "defaultValue": 3, + "metadata": { + "description": "The number of agent nodes for the cluster." + }, + "minValue": 1, + "maxValue": 50 + }, + "agentVMSize": { + "type": "string", + "defaultValue": "Standard_D2_v2", + "metadata": { + "description": "The size of the Virtual Machine." + } + }, + "servicePrincipalClientId": { + "metadata": { + "description": "Client ID (used by cloudprovider)" + }, + "type": "securestring" + }, + "servicePrincipalClientSecret": { + "metadata": { + "description": "The Service Principal Client Secret." + }, + "type": "securestring" + }, + "osType": { + "type": "string", + "defaultValue": "Linux", + "allowedValues": [ + "Linux" + ], + "metadata": { + "description": "The type of operating system." + } + }, + "kubernetesVersion": { + "type": "string", + "defaultValue": "1.10.7", + "metadata": { + "description": "The version of Kubernetes." + }, + "allowedValues": [ + "1.10.7", + "1.9.6" + ] + }, + "enableOmsAgent": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "boolean flag to turn on and off of omsagent addon" + } + }, + "workspaceRegion": { + "type": "string", + "defaultValue": "East US", + "metadata": { + "description": "Specify the region for your OMS workspace" + } + }, + "workspaceName": { + "type": "string", + "metadata": { + "description": "Specify the name of the OMS workspace" + } + }, + "omsWorkspaceId": { + "type": "string", + "metadata": { + "description": "Specify the resource id of the OMS workspace" + } + }, + "omsSku": { + "type": "string", + "defaultValue": "standalone", + "allowedValues": [ + "free", + "standalone", + "pernode" + ], + "metadata": { + "description": "Select the SKU for your workspace" + } + }, + "enableHttpApplicationRouting": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "boolean flag to turn on and off of http application routing" + } + }, + "networkPlugin": { + "type": "string", + "defaultValue": "azure", + "allowedValues": [ + "azure", + "kubenet" + ], + "metadata": { + "description": "Network plugin used for building Kubernetes network." + } + }, + "maxPods": { + "type": "int", + "defaultValue": 30, + "metadata": { + "description": "Maximum number of pods that can run on a node." + } + }, + "vnetSubnetID": { + "type": "string", + "metadata": { + "description": "Resource ID of virtual network subnet used for nodes and/or pods IP assignment." + } + }, + "serviceCidr": { + "type": "string", + "metadata": { + "description": "A CIDR notation IP range from which to assign service cluster IPs." + } + }, + "dnsServiceIP": { + "type": "string", + "metadata": { + "description": "Containers DNS server IP address." + } + }, + "dockerBridgeCidr": { + "type": "string", + "defaultValue": "172.17.0.1/16", + "metadata": { + "description": "A CIDR notation IP for Docker bridge." + } + } + }, + "variables": { + "environmentPrefix": "[if(equals(parameters('environment'),'production'),'prod','int')]", + "locationAffix1": "eus" + }, + "resources": [ + { + "name": "[parameters('resourceName')]", + "apiVersion": "2018-03-31", + "dependsOn": [ + "Microsoft.Network/virtualNetworks/aks-vnet" + ], + "type": "Microsoft.ContainerService/managedClusters", + "location": "[parameters('location')]", + "properties": { + "kubernetesVersion": "[parameters('kubernetesVersion')]", + "enableRBAC": false, + "dnsPrefix": "[parameters('dnsPrefix')]", + "addonProfiles": { + "httpApplicationRouting": { + "enabled": "[parameters('enableHttpApplicationRouting')]" + }, + "omsagent": { + "enabled": "[parameters('enableOmsAgent')]", + "config": { + "logAnalyticsWorkspaceResourceID": "[parameters('omsWorkspaceId')]" + } + } + }, + "agentPoolProfiles": [ + { + "name": "agentpool", + "osDiskSizeGB": 0, + "count": "[parameters('agentCount')]", + "vmSize": "[parameters('agentVMSize')]", + "osType": "[parameters('osType')]", + "storageProfile": "ManagedDisks", + "vnetSubnetID": "[parameters('vnetSubnetID')]" + } + ], + "servicePrincipalProfile": { + "ClientId": "[parameters('servicePrincipalClientId')]", + "Secret": "[parameters('servicePrincipalClientSecret')]" + }, + "networkProfile": { + "networkPlugin": "[parameters('networkPlugin')]", + "serviceCidr": "[parameters('serviceCidr')]", + "dnsServiceIP": "[parameters('dnsServiceIP')]", + "dockerBridgeCidr": "[parameters('dockerBridgeCidr')]" + } + }, + "tags": {} + }, + { + "type": "Microsoft.Resources/deployments", + "name": "SolutionDeployment", + "apiVersion": "2017-05-10", + "resourceGroup": "[split(parameters('omsWorkspaceId'),'/')[4]]", + "subscriptionId": "[split(parameters('omsWorkspaceId'),'/')[2]]", + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "variables": {}, + "resources": [ + { + "apiVersion": "2015-11-01-preview", + "type": "Microsoft.OperationsManagement/solutions", + "location": "[parameters('workspaceRegion')]", + "name": "[concat('ContainerInsights', '(', split(parameters('omsWorkspaceId'),'/')[8], ')')]", + "properties": { + "workspaceResourceId": "[parameters('omsWorkspaceId')]" + }, + "plan": { + "name": "[concat('ContainerInsights', '(', split(parameters('omsWorkspaceId'),'/')[8], ')')]", + "product": "[concat('OMSGallery/', 'ContainerInsights')]", + "promotionCode": "", + "publisher": "Microsoft" + } + } + ] + } + }, + "dependsOn": [] + }, + { + "name": "aks-vnet", + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2018-02-01", + "location": "[parameters('location')]", + "properties": { + "addressSpace": { + "addressPrefixes": [ + "10.1.0.0/16" + ] + }, + "subnets": [ + { + "name": "aks", + "properties": { + "addressPrefix": "10.1.1.0/24" + } + } + ] + } + } + ], + "outputs": { + "controlPlaneFQDN": { + "type": "string", + "value": "[reference(concat('Microsoft.ContainerService/managedClusters/', parameters('resourceName'))).fqdn]" + } + } +} diff --git a/packages/psdocs/docs/scenarios/benchmark/results-v0.7.0.md b/packages/psdocs/docs/scenarios/benchmark/results-v0.7.0.md new file mode 100644 index 00000000..fc97eddb --- /dev/null +++ b/packages/psdocs/docs/scenarios/benchmark/results-v0.7.0.md @@ -0,0 +1,14 @@ +``` ini + +BenchmarkDotNet=v0.12.1, OS=Windows 10.0.19041.450 (2004/?/20H1) +Intel Core i7-1065G7 CPU 1.30GHz, 1 CPU, 8 logical and 4 physical cores +.NET Core SDK=3.1.401 + [Host] : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT + DefaultJob : .NET Core 3.1.7 (CoreCLR 4.700.20.36602, CoreFX 4.700.20.37001), X64 RyuJIT + + +``` +| Method | Mean | Error | StdDev | Gen 0 | Gen 1 | Gen 2 | Allocated | +|------------------------ |---------------:|-------------:|-------------:|---------:|-------:|------:|----------:| +| InvokeMarkdownProcessor | 239.9 ns | 2.33 ns | 1.94 ns | 0.1450 | - | - | 608 B | +| InvokePipeline | 1,959,246.0 ns | 39,108.35 ns | 56,088.03 ns | 175.7813 | 3.9063 | - | 761570 B | diff --git a/packages/psdocs/docs/scenarios/directory/directory.md b/packages/psdocs/docs/scenarios/directory/directory.md new file mode 100644 index 00000000..ed247910 --- /dev/null +++ b/packages/psdocs/docs/scenarios/directory/directory.md @@ -0,0 +1,36 @@ +# Generate a document from a directory listing + +```powershell +# File: Sample.Doc.ps1 + +# Define a document called Sample +Document Sample { + + # Add an introduction section + Section Introduction { + # Add a comment + "This is a sample file list from $TargetObject" + + # Generate a table + Get-ChildItem -Path $TargetObject | Table -Property Name,PSIsContainer + } +} +``` + +To execute the document use `Invoke-PSDocument`. + +For example: + +```powershell +Invoke-PSDocument -InputObject 'C:\'; +``` + +```powershell +# Import PSDocs module +Import-Module -Name PSDocs; + +# Call the document definition as a function to generate markdown from an object +Invoke-PSDocument -InputObject 'C:\'; +``` + +An example of the output generated is available [here](../../examples/Get-child-item-output.md). diff --git a/packages/psdocs/docs/scenarios/docfx/integration-with-docfx.md b/packages/psdocs/docs/scenarios/docfx/integration-with-docfx.md new file mode 100644 index 00000000..1cf79919 --- /dev/null +++ b/packages/psdocs/docs/scenarios/docfx/integration-with-docfx.md @@ -0,0 +1,53 @@ +# Integration with DocFX + +DocFX is a open source tool that converts markdown documentation into HTML. PSDocs can be used to dynamically generate markdown that can be processed by DocFX. + +DocFX uses a `docfx.json` to determine what content to include and how to process each file. To process markdown files a `build.content` section should be added to reference the output location where PSDocs will generate markdown relative to the location of `docfx.json`. + +An example `docfx.json` is provided below. + +```json +{ + "metadata": [ + ], + "build": { + "content": [ + { + "files": [ + "**/**.md", + "**/toc.yml", + "*.md", + "toc.yml" + ] + } + ], + "resource": [ + { + "files": [ + "**/media/**" + ] + } + ], + "overwrite": [ + { + "files": [ ], + "exclude": [ + "out/**", + "build/**" + ] + } + ], + "dest": "_site", + "globalMetadataFiles": [], + "fileMetadataFiles": [], + "template": [ + "default" + ], + "postProcessors": [], + "noLangKeyword": false, + "keepFileLink": false, + "cleanupCacheHistory": false, + "disableGitFeatures": false + } +} +``` diff --git a/packages/psdocs/docs/scenarios/dsc-mof/dsc-mof.md b/packages/psdocs/docs/scenarios/dsc-mof/dsc-mof.md new file mode 100644 index 00000000..94466e37 --- /dev/null +++ b/packages/psdocs/docs/scenarios/dsc-mof/dsc-mof.md @@ -0,0 +1,22 @@ +# Generate documentation from Desired State Configuration + +```powershell +# Import PSDocs.Dsc module +Import-Module -Name PSDocs.Dsc; + +# Define a document called Sample +Document 'Sample' { + + # Add an 'Installed features' section in the document + Section 'Installed features' { + # Add a comment + 'The following Windows features have been installed.' + + # Generate a table of Windows Features + $TargetObject.ResourceType.WindowsFeature | Table -Property Name,Ensure + } +} + +# Call the document definition and generate markdown for each .mof file in the .\nodes directory +Invoke-DscNodeDocument -DocumentName 'Sample' -Path '.\nodes' -OutputPath '.\docs'; +``` diff --git a/packages/psdocs/docs/upgrade-notes.md b/packages/psdocs/docs/upgrade-notes.md new file mode 100644 index 00000000..1246464e --- /dev/null +++ b/packages/psdocs/docs/upgrade-notes.md @@ -0,0 +1,170 @@ +# Upgrade notes + +This document contains notes to help upgrade from previous versions of PSDocs. + +## Upgrade to v0.9.0 + +Follow these notes to upgrade to v0.9.0 from previous versions. + +### Empty documents + +Previously output would be generated from any `Document` block. + +For example: + +```powershell +Document 'WithTitle' { + Title 'Read me' + Metadata @{ + key = 'value' + } +} +``` + +Documents that do not generate any body content are ignored. +If a document **only** sets a title or metadata the document is ignored. +To generate a document, set any body content. + +For example: + +```powershell +Document 'WithText' { + Title 'Read me' + + 'Some content' +} + +Document 'WithBlockQuote' { + Title 'Read me' + + 'Some content' | BlockQuote +} +``` + +## Upgrade to v0.7.0 + +Follow these notes to upgrade to v0.7.0 from previous versions. + +### Inline blocks + +Previously a `Document` block and the command that generated the document could be called inline within the same script. + +For example: + +```powershell +Document 'SampleMessage' { + 'Testing 123.' +} + +Sample -InputObject @{ } +``` + +Support for inline `Document` blocks has been removed. +Use the following steps to migrate inline blocks to a file if you previously used this feature: + +- Create a new file ending with `.Doc.ps1` file extension. +For example `Sample.Doc.ps1`. +- Copy and paste the previous inline `Document` block into the file. +Multiple document blocks can be included in the same file. +- Update command-line to use `Invoke-PSDocument` instead of using the name of the document block directly. + +Example `Sample.Doc.ps1`: + +```powershell +Document 'SampleMessage' { + 'Testing 123.' +} +``` + +The cmdlet `Invoke-PSDocument` can be called as follows: + +```powershell +Invoke-PSDocument -Path .\Sample.Doc.ps1 -Name SampleMessage; +``` + +On Linux, the file extension `.doc.ps1` is not automatically found by PSDocs because of file system case-sensitivity. +For consistency, use `.Doc.ps1` on all platforms. + +### Helper functions + +Previously helper functions could be defined with the default scope. + +For example: + +```powershell +function SampleHelperFn { + # Do something +} + +Document 'SampleDocument' { + SampleHelperFn; +} +``` + +The execution model of PSDocs now uses a separate runspace sandbox. +Blocks are enumerated first then executed. +Helper functions can still be used however must be flagged with the global scope modifier. + +For example: + +```powershell +function global:SampleHelperFn { + # Do something +} + +Document 'SampleDocument' { + SampleHelperFn; +} +``` + +### Script block usage of Note and Warning + +Previously `Note` and `Warning` blocks could contain a script block. + +For example: + +```powershell +Document 'NoteScriptBlock' { + Note { + 'A note' + } +} +``` + +This syntax was deprecated as of v0.6.0, and has been removed as of v0.7.0. +Instead of using a script block, pipe the note or warning text to `Note`, `Warning`. + +For example: + +```powershell +Document 'NotePipe' { + 'A note' | Note +} +``` + +### Section -When parameter + +Previously the `-When` parameter could be used with the section keyword. + +For example: + +```powershell +Document 'SectionWhen' { + Section -When { $i -eq 0 } { + 'Sample section text.' + } +} +``` + +The `-When` parameter was replaced with `-If` as of v0.6.0, and has been removed as of v0.7.0. +Instead of using `-When`, update section blocks to use `-If`. + +For example: + +```powershell +Document 'SectionIf' { + Section -If { $i -eq 0 } { + 'Sample section text.' + } +} +``` diff --git a/packages/psdocs/modules.json b/packages/psdocs/modules.json new file mode 100644 index 00000000..ce934805 --- /dev/null +++ b/packages/psdocs/modules.json @@ -0,0 +1,14 @@ +{ + "dependencies": {}, + "devDependencies": { + "Pester": { + "version": "5.6.1" + }, + "platyPS": { + "version": "0.14.2" + }, + "PSScriptAnalyzer": { + "version": "1.21.0" + } + } +} diff --git a/packages/psdocs/pipeline.build.ps1 b/packages/psdocs/pipeline.build.ps1 new file mode 100644 index 00000000..19e179e3 --- /dev/null +++ b/packages/psdocs/pipeline.build.ps1 @@ -0,0 +1,252 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $False)] + [String]$Build = '0.0.1', + + [Parameter(Mandatory = $False)] + [String]$Configuration = 'Debug', + + [Parameter(Mandatory = $False)] + [String]$ApiKey, + + [Parameter(Mandatory = $False)] + [Switch]$CodeCoverage = $False, + + [Parameter(Mandatory = $False)] + [Switch]$Benchmark = $False, + + [Parameter(Mandatory = $False)] + [String]$ArtifactPath = (Join-Path -Path $PWD -ChildPath out/modules), + + [Parameter(Mandatory = $False)] + [String]$TestGroup = $Null +) + +Write-Host -Object "[Pipeline] -- PowerShell v$($PSVersionTable.PSVersion.ToString())" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- PWD: $PWD" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- ArtifactPath: $ArtifactPath" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- BuildNumber: $($Env:BUILD_BUILDNUMBER)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- SourceBranch: $($Env:BUILD_SOURCEBRANCH)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- SourceBranchName: $($Env:BUILD_SOURCEBRANCHNAME)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- Culture: $((Get-Culture).Name), $((Get-Culture).Parent)" -ForegroundColor Green; + +if ($Env:SYSTEM_DEBUG -eq 'true') { + $VerbosePreference = 'Continue'; +} + +if ($Env:BUILD_SOURCEBRANCH -like '*/tags/*' -and $Env:BUILD_SOURCEBRANCHNAME -like 'v0.*') { + $Build = $Env:BUILD_SOURCEBRANCHNAME.Substring(1); +} + +$version = $Build; +$versionSuffix = [String]::Empty; + +if ($version -like '*-*') { + [String[]]$versionParts = $version.Split('-', [System.StringSplitOptions]::RemoveEmptyEntries); + $version = $versionParts[0]; + + if ($versionParts.Length -eq 2) { + $versionSuffix = $versionParts[1]; + } +} + +Write-Host -Object "[Pipeline] -- Using version: $version" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- Using versionSuffix: $versionSuffix" -ForegroundColor Green; + +if ($Env:COVERAGE -eq 'true') { + $CodeCoverage = $True; +} + +# Copy the PowerShell modules files to the destination path +function CopyModuleFiles { + + param ( + [Parameter(Mandatory = $True)] + [String]$Path, + + [Parameter(Mandatory = $True)] + [String]$DestinationPath + ) + + process { + $sourcePath = Resolve-Path -Path $Path; + + Get-ChildItem -Path $sourcePath -Recurse -File -Include *.ps1,*.psm1,*.psd1,*.ps1xml | Where-Object -FilterScript { + ($_.FullName -notmatch '(\.(cs|csproj)|(\\|\/)(obj|bin))') + } | ForEach-Object -Process { + $filePath = $_.FullName.Replace($sourcePath, $destinationPath); + + $parentPath = Split-Path -Path $filePath -Parent; + + if (!(Test-Path -Path $parentPath)) { + $Null = New-Item -Path $parentPath -ItemType Directory -Force; + } + + Copy-Item -Path $_.FullName -Destination $filePath -Force; + }; + } +} + +task BuildDotNet { + exec { + dotnet restore + } + exec { + # Build library + dotnet publish src/PSDocs -c $Configuration -f netstandard2.0 -o $(Join-Path -Path $PWD -ChildPath out/modules/PSDocs) -p:version=$Build + } +} + +task TestDotNet { + if ($CodeCoverage) { + exec { + # Test library + dotnet test -r (Join-Path $PWD -ChildPath reports/) tests/PSDocs.Tests + } + } + else { + exec { + # Test library + dotnet test tests/PSDocs.Tests + } + } +} + +task CopyModule { + CopyModuleFiles -Path src/PSDocs -DestinationPath out/modules/PSDocs; + + # Copy third party notices + Copy-Item -Path ThirdPartyNotices.txt -Destination out/modules/PSDocs; +} + +# Synopsis: Build modules only +task BuildModule BuildDotNet, CopyModule, VersionModule + +# Synopsis: Build help +task BuildHelp BuildModule, Dependencies, { + # Avoid YamlDotNet issue in same app domain + exec { + $pwshPath = (Get-Process -Id $PID).Path; + &$pwshPath -Command { + # Generate MAML and about topics + Import-Module -Name PlatyPS -Verbose:$False; + $Null = New-ExternalHelp -OutputPath 'out/docs/PSDocs' -Path '.\docs\commands\PSDocs\en-US','.\docs\keywords\PSDocs\en-US', '.\docs\concepts\PSDocs\en-US' -Force; + } + } + + if (!(Test-Path -Path 'out/docs/PSDocs/PSDocs-help.xml')) { + throw 'Failed find generated cmdlet help.'; + } + + # Copy generated help into module out path + $Null = Copy-Item -Path out/docs/PSDocs/* -Destination out/modules/PSDocs/en-US; + $Null = Copy-Item -Path out/docs/PSDocs/* -Destination out/modules/PSDocs/en-AU; + $Null = Copy-Item -Path out/docs/PSDocs/* -Destination out/modules/PSDocs/en-GB; +} + +task ScaffoldHelp BuildModule, { + Import-Module (Join-Path -Path $PWD -ChildPath out/modules/PSDocs) -Force; + Update-MarkdownHelp -Path '.\docs\commands\PSDocs\en-US'; +} + +# Synopsis: Remove temp files. +task Clean { + Remove-Item -Path out,reports -Recurse -Force -ErrorAction SilentlyContinue; +} + +task VersionModule { + $modulePath = Join-Path -Path $ArtifactPath -ChildPath 'PSDocs'; + $manifestPath = Join-Path -Path $modulePath -ChildPath 'PSDocs.psd1'; + Write-Verbose -Message "[VersionModule] -- Checking module path: $modulePath"; + + if (![String]::IsNullOrEmpty($Build)) { + # Update module version + if (![String]::IsNullOrEmpty($version)) { + Write-Verbose -Message "[VersionModule] -- Updating module manifest ModuleVersion"; + Update-ModuleManifest -Path $manifestPath -ModuleVersion $version; + } + + # Update pre-release version + if (![String]::IsNullOrEmpty($versionSuffix)) { + Write-Verbose -Message "[VersionModule] -- Updating module manifest Prerelease"; + Update-ModuleManifest -Path $manifestPath -Prerelease $versionSuffix; + } + } +} + +# Synopsis: Install NuGet provider +task NuGet { + if ($Null -eq (Get-PackageProvider -Name NuGet -ErrorAction Ignore)) { + Install-PackageProvider -Name NuGet -Force -Scope CurrentUser; + } +} + +task Dependencies NuGet, { + Import-Module $PWD/scripts/dependencies.psm1; + Install-Dependencies -Path $PWD/modules.json -Dev; +} + +# Synopsis: Test the module +task TestModule Dependencies, { + Import-Module Pester -RequiredVersion 5.6.1 -Force; + + # Define Pester configuration + $pesterConfig = [PesterConfiguration]::Default + $pesterConfig.Run.PassThru = $True + # Enable NUnitXml output + $pesterConfig.TestResult.OutputFormat = "NUnitXml" + $pesterConfig.TestResult.OutputPath = 'reports/pester-unit.xml' + $pesterConfig.TestResult.Enabled = $True + + if ($CodeCoverage) { + $pesterConfig.CodeCoverage.OutputFormat = 'JaCoCo' + $pesterConfig.CodeCoverage.OutputPath = 'reports/pester-coverage.xml' + $pesterConfig.CodeCoverage.Path = (Join-Path -Path $PWD -ChildPath 'out/modules/**/*.psm1') + } + + if (!(Test-Path -Path reports)) { + $Null = New-Item -Path reports -ItemType Directory -Force; + } + + if ($Null -ne $TestGroup) { + $pesterConfig.Filter.Tag = $TestGroup + } + + # Run Pester tests + $results = Invoke-Pester -Configuration $pesterConfig + + # Throw an error if Pester tests failed + if ($Null -eq $results) { + throw 'Failed to get Pester test results.' + } + elseif ($results.Result.FailedCount -gt 0) { + throw "$($results.Result.FailedCount) tests failed." + } +} + + +task Benchmark { + if ($Benchmark -or $BuildTask -eq 'Benchmark') { + dotnet run --project src/PSDocs.Benchmark -f net7.0 -c Release -- benchmark --output $PWD; + } +} + +# Synopsis: Run script analyzer +task Analyze Build, Dependencies, { + Invoke-ScriptAnalyzer -Path out/modules/PSDocs; +} + +# Synopsis: Remove temp files. +task Clean { + Remove-Item -Path out,reports -Recurse -Force -ErrorAction SilentlyContinue; +} + +task Build Clean, BuildModule, VersionModule, BuildHelp + +task Test Build, TestDotNet, TestModule + +# Synopsis: Build and test. Entry point for CI Build stage +task . Build, TestDotNet diff --git a/packages/psdocs/ps-docs.yaml b/packages/psdocs/ps-docs.yaml new file mode 100644 index 00000000..200cb3c6 --- /dev/null +++ b/packages/psdocs/ps-docs.yaml @@ -0,0 +1,3 @@ + +output: + culture: 'en-US' diff --git a/packages/psdocs/ps-project.yaml b/packages/psdocs/ps-project.yaml new file mode 100644 index 00000000..be3921f3 --- /dev/null +++ b/packages/psdocs/ps-project.yaml @@ -0,0 +1,17 @@ +# +# PSDocs +# + +info: + name: PSDocs + +repository: + type: git + url: https://github.com/Microsoft/PSDocs.git + +tasks: + clear: + steps: + - gitPrune: + name: origin + removeGone: true diff --git a/packages/psdocs/ps-rule.yaml b/packages/psdocs/ps-rule.yaml new file mode 100644 index 00000000..1026d999 --- /dev/null +++ b/packages/psdocs/ps-rule.yaml @@ -0,0 +1,15 @@ +# +# PSRule configuration +# + +# Please see the documentation for all configuration options: +# https://microsoft.github.io/PSRule/ + +output: + culture: + - 'en-US' + +input: + pathIgnore: + - '*.Designer.cs' + - '*.md' diff --git a/packages/psdocs/schemas/PSDocs-language.schema.json b/packages/psdocs/schemas/PSDocs-language.schema.json new file mode 100644 index 00000000..37df49b7 --- /dev/null +++ b/packages/psdocs/schemas/PSDocs-language.schema.json @@ -0,0 +1,627 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "PSDocs language", + "description": "A schema for PSDocs YAML language files.", + "oneOf": [ + { + "$ref": "#/definitions/selector-v1" + } + ], + "definitions": { + "resource-metadata": { + "type": "object", + "title": "Metadata", + "description": "Additional information to identify the resource.", + "properties": { + "name": { + "type": "string", + "title": "Name", + "description": "The name of the resource. This must be unique.", + "minLength": 3 + }, + "annotations": { + "type": "object", + "title": "Annotations" + } + }, + "required": [ + "name" + ] + }, + "selector-v1": { + "type": "object", + "title": "Selector", + "description": "A PSDocs Selector.", + "markdownDescription": "A PSDocs Selector. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md)", + "properties": { + "apiVersion": { + "type": "string", + "title": "API Version", + "description": "The API Version for the PSDocs resources.", + "enum": [ + "github.com/microsoft/PSDocs/v1" + ] + }, + "kind": { + "type": "string", + "title": "Kind", + "description": "A PSDocs Selector resource.", + "enum": [ + "Selector" + ] + }, + "metadata": { + "type": "object", + "$ref": "#/definitions/resource-metadata" + }, + "spec": { + "type": "object", + "$ref": "#/definitions/selectorSpec" + } + }, + "required": [ + "apiVersion", + "kind", + "metadata", + "spec" + ] + }, + "selectorSpec": { + "type": "object", + "title": "Spec", + "description": "PSDocs selector specification.", + "properties": { + "if": { + "type": "object", + "$ref": "#/definitions/selectorExpression" + } + }, + "required": [ + "if" + ], + "additionalProperties": false + }, + "selectorExpression": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/selectorOperator" + }, + { + "$ref": "#/definitions/selectorCondition" + } + ] + }, + "selectorOperator": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/selectorOperatorAllOf" + }, + { + "$ref": "#/definitions/selectorOperatorAnyOf" + }, + { + "$ref": "#/definitions/selectorOperatorNot" + } + ] + }, + "selectorCondition": { + "type": "object", + "oneOf": [ + { + "$ref": "#/definitions/selectorConditionExists" + }, + { + "$ref": "#/definitions/selectorConditionEquals" + }, + { + "$ref": "#/definitions/selectorConditionNotEquals" + }, + { + "$ref": "#/definitions/selectorConditionHasValue" + }, + { + "$ref": "#/definitions/selectorConditionMatch" + }, + { + "$ref": "#/definitions/selectorConditionNotMatch" + }, + { + "$ref": "#/definitions/selectorConditionIn" + }, + { + "$ref": "#/definitions/selectorConditionNotIn" + }, + { + "$ref": "#/definitions/selectorConditionLess" + }, + { + "$ref": "#/definitions/selectorConditionLessOrEquals" + }, + { + "$ref": "#/definitions/selectorConditionGreater" + }, + { + "$ref": "#/definitions/selectorConditionGreaterOrEquals" + }, + { + "$ref": "#/definitions/selectorConditionStartsWith" + }, + { + "$ref": "#/definitions/selectorConditionEndsWith" + }, + { + "$ref": "#/definitions/selectorConditionContains" + }, + { + "$ref": "#/definitions/selectorConditionIsString" + }, + { + "$ref": "#/definitions/selectorConditionIsLower" + }, + { + "$ref": "#/definitions/selectorConditionIsUpper" + } + ] + }, + "selectorProperties": { + "oneOf": [ + { + "properties": { + "field": { + "type": "string", + "title": "Field", + "description": "The path of the field.", + "markdownDescription": "The path of the field. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#field)" + } + }, + "required": [ + "field" + ] + } + ] + }, + "selectorOperatorAllOf": { + "type": "object", + "properties": { + "allOf": { + "type": "array", + "title": "AllOf", + "description": "All of the expressions must be true.", + "markdownDescription": "All of the expressions must be true. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#allof)", + "items": { + "$ref": "#/definitions/selectorExpression" + } + } + }, + "required": [ + "allOf" + ], + "additionalProperties": false + }, + "selectorOperatorAnyOf": { + "type": "object", + "properties": { + "anyOf": { + "type": "array", + "title": "AnyOf", + "description": "One of the expressions must be true.", + "markdownDescription": "All of the expressions must be true. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#anyof)", + "items": { + "$ref": "#/definitions/selectorExpression" + } + } + }, + "required": [ + "anyOf" + ], + "additionalProperties": false + }, + "selectorOperatorNot": { + "type": "object", + "properties": { + "not": { + "type": "object", + "title": "Not", + "description": "The nested expression must not be true.", + "markdownDescription": "The nested expression must not be true. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#not)", + "$ref": "#/definitions/selectorExpression" + } + }, + "required": [ + "not" + ] + }, + "selectorConditionExists": { + "type": "object", + "properties": { + "exists": { + "type": "boolean", + "title": "Exists", + "description": "Must have the named field.", + "markdownDescription": "Must have the named field. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#exists)" + }, + "field": { + "type": "string", + "title": "Field", + "description": "The path of the field.", + "markdownDescription": "The path of the field. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#field)" + } + }, + "required": [ + "exists", + "field" + ] + }, + "selectorConditionEquals": { + "type": "object", + "properties": { + "equals": { + "oneOf": [ + { + "type": "string", + "title": "Equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#equals)" + }, + { + "type": "integer", + "title": "Equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#equals)" + }, + { + "type": "boolean", + "title": "Equals", + "description": "Must have the specified value.", + "markdownDescription": "Must have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#equals)" + } + ] + } + }, + "required": [ + "equals" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionNotEquals": { + "type": "object", + "properties": { + "notEquals": { + "oneOf": [ + { + "type": "string", + "title": "Not Equals", + "description": "Must not have the specified value.", + "markdownDescription": "Must not have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notequals)" + }, + { + "type": "integer", + "title": "Not Equals", + "description": "Must not have the specified value.", + "markdownDescription": "Must not have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notequals)" + }, + { + "type": "boolean", + "title": "Not Equals", + "description": "Must not have the specified value.", + "markdownDescription": "Must not have the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notequals)" + } + ] + } + }, + "required": [ + "notEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionHasValue": { + "type": "object", + "properties": { + "hasValue": { + "type": "boolean", + "title": "Has Value", + "description": "Must have a non-empty value.", + "markdownDescription": "Must have a non-empty value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#hasvalue)" + } + }, + "required": [ + "hasValue" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionMatch": { + "type": "object", + "properties": { + "match": { + "type": "string", + "title": "Match", + "description": "Must match the regular expression.", + "markdownDescription": "Must match the regular expression. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#match)" + } + }, + "required": [ + "match" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionNotMatch": { + "type": "object", + "properties": { + "notMatch": { + "type": "string", + "title": "Not Match", + "description": "Must not match the regular expression.", + "markdownDescription": "Must not match the regular expression. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notmatch)" + } + }, + "required": [ + "notMatch" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIn": { + "type": "object", + "properties": { + "in": { + "type": "array", + "title": "In", + "description": "Must equal one the values.", + "markdownDescription": "Must equal one the values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#in)" + } + }, + "required": [ + "in" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionNotIn": { + "type": "object", + "properties": { + "notIn": { + "type": "array", + "title": "Not In", + "description": "Must not equal any of the values.", + "markdownDescription": "Must not equal one the values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#notin)" + } + }, + "required": [ + "notIn" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionLess": { + "type": "object", + "properties": { + "less": { + "type": "integer", + "title": "Less", + "description": "Must be less then the specified value.", + "markdownDescription": "Must be less then the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#less)" + } + }, + "required": [ + "less" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionLessOrEquals": { + "type": "object", + "properties": { + "lessOrEquals": { + "type": "integer", + "title": "Less or Equal to", + "description": "Must be less or equal to the specified value.", + "markdownDescription": "Must be less or equal to the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#lessorequals)" + } + }, + "required": [ + "lessOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionGreater": { + "type": "object", + "properties": { + "greater": { + "type": "integer", + "title": "Greater", + "description": "Must be greater then the specified value.", + "markdownDescription": "Must be greater then the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greater)" + } + }, + "required": [ + "greater" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionGreaterOrEquals": { + "type": "object", + "properties": { + "greaterOrEquals": { + "type": "integer", + "title": "Greater or Equal to", + "description": "Must be greater or equal to the specified value.", + "markdownDescription": "Must be greater or equal to the specified value. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#greaterorequals)" + } + }, + "required": [ + "greaterOrEquals" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionStartsWith": { + "type": "object", + "properties": { + "startsWith": { + "title": "Starts with", + "description": "Must start with one of the specified values.", + "markdownDescription": "Must start with one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#startswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "startsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionEndsWith": { + "type": "object", + "properties": { + "endsWith": { + "title": "Ends with", + "description": "Must end with one of the specified values.", + "markdownDescription": "Must end with one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#endswith)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "endsWith" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionContains": { + "type": "object", + "properties": { + "contains": { + "title": "Contains", + "description": "Must contain one of the specified values.", + "markdownDescription": "Must contain one of the specified values. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#contains)", + "$ref": "#/definitions/selectorExpressionValueMultiString" + } + }, + "required": [ + "contains" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsString": { + "type": "object", + "properties": { + "isString": { + "type": "boolean", + "title": "Is string", + "description": "Must be a string type.", + "markdownDescription": "Must be a string type. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isstring)" + } + }, + "required": [ + "isString" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsLower": { + "type": "object", + "properties": { + "isLower": { + "type": "boolean", + "title": "Is Lowercase", + "description": "Must be a lowercase string.", + "markdownDescription": "Must be a lowercase string. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#islower)" + } + }, + "required": [ + "isLower" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorConditionIsUpper": { + "type": "object", + "properties": { + "isUpper": { + "type": "boolean", + "title": "Is Uppercase", + "description": "Must be an uppercase string.", + "markdownDescription": "Must be an uppercase string. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Selectors.md#isupper)" + } + }, + "required": [ + "isUpper" + ], + "oneOf": [ + { + "$ref": "#/definitions/selectorProperties" + } + ] + }, + "selectorExpressionValueMultiString": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + }, + "uniqueItems": true + } + ] + } + } +} diff --git a/packages/psdocs/schemas/PSDocs-options.schema.json b/packages/psdocs/schemas/PSDocs-options.schema.json new file mode 100644 index 00000000..41927274 --- /dev/null +++ b/packages/psdocs/schemas/PSDocs-options.schema.json @@ -0,0 +1,213 @@ +{ + "$schema": "https://json-schema.org/draft-07/schema#", + "title": "PSDocs options", + "description": "A schema for PSDocs YAML options files.", + "$ref": "#/definitions/options", + "definitions": { + "configuration": { + "type": "object", + "title": "Configuration values", + "description": "A set of key/ value configuration options for document definitions.", + "markdownDescription": "A set of key/ value configuration options for document definitions. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#configuration)", + "defaultSnippets": [ + { + "label": "Configuration value", + "body": { + "${1:Key}": "${2:Value}" + } + } + ] + }, + "execution-option": { + "type": "object", + "title": "Execution options", + "description": "Options that affect document execution.", + "properties": { + "languageMode": { + "type": "string", + "title": "Language mode", + "description": "The PowerShell language mode to use for document execution. The default is FullLanguage.", + "markdownDescription": "The PowerShell language mode to use for document execution. The default is `FullLanguage`. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#executionlanguagemode)", + "enum": [ + "FullLanguage", + "ConstrainedLanguage" + ], + "default": "FullLanguage" + } + }, + "additionalProperties": false + }, + "input-option": { + "type": "object", + "title": "Input options", + "description": "Options that affect how input types are processed.", + "properties": { + "format": { + "type": "string", + "title": "Input format", + "description": "The input string format. The default is Detect, which will try to detect the format when the -InputPath parameter is used.", + "markdownDescription": "The input string format. The default is `Detect`, which will try to detect the format when the `-InputPath` parameter is used. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#inputformat)", + "enum": [ + "None", + "Yaml", + "Json", + "PowerShellData", + "Detect" + ], + "default": "Detect" + }, + "objectPath": { + "type": "string", + "title": "Object path", + "description": "The object path to a property to use instead of the pipeline object.", + "markdownDescription": "The object path to a property to use instead of the pipeline object. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#inputobjectpath)" + }, + "pathIgnore": { + "type": "array", + "title": "Path ignore", + "description": "Ignores input files that match the path spec.", + "markdownDescription": "Ignores input files that match the path spec. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#inputpathignore)", + "items": { + "type": "string" + }, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + "markdown-option": { + "type": "object", + "title": "Markdown options", + "description": "Options that affect markdown formatting.", + "properties": { + "columnPadding": { + "type": "string", + "title": "Column padding", + "description": "Determines how table columns are padded. By default (MatchHeader), pads the header with a single space, then pads the column value, to the same width as the header.", + "markdownDescription": "Determines how table columns are padded. By default (`MatchHeader`), pads the header with a single space, then pads the column value, to the same width as the header. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdowncolumnpadding)", + "enum": [ + "None", + "Single", + "MatchHeader", + "Undefined" + ], + "default": "MatchHeader" + }, + "encoding": { + "type": "string", + "title": "Encoding", + "description": "Sets the text encoding used for markdown output files. By default (Default), UTF-8 without byte order mark (BOM) is used.", + "markdownDescription": "Sets the text encoding used for markdown output files. By default (`Default`), UTF-8 without byte order mark (BOM) is used. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownencoding)", + "enum": [ + "Default", + "UTF8", + "UTF7", + "Unicode", + "UTF32", + "ASCII" + ], + "default": "Default" + }, + "skipEmptySections": { + "type": "boolean", + "title": "Skip empty sections", + "description": "Determines if empty sections are included in output. By default, sections without content are not included in output.", + "markdownDescription": "Determines if empty sections are included in output. By default, sections without content are not included in output. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownskipemptysections)", + "default": true + }, + "useEdgePipes": { + "type": "string", + "title": "Use edge pipes", + "description": "Determines when pipes on the edge of a table should be used. By default (WhenRequired), edge pipes are only used when required.", + "markdownDescription": "Determines when pipes on the edge of a table should be used. By default (`WhenRequired`), edge pipes are only used when required. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownuseedgepipes)", + "enum": [ + "WhenRequired", + "Always" + ], + "default": "WhenRequired" + }, + "wrapSeparator": { + "type": "string", + "title": "Wrap separator", + "description": "Specifies the character/ string to use when wrapping lines in a table cell. By default a space is used.", + "markdownDescription": "Specifies the character/ string to use when wrapping lines in a table cell. By default a space (` `) is used. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#markdownwrapseparator)", + "default": " " + } + }, + "additionalProperties": false + }, + "output-option": { + "type": "object", + "title": "Output options", + "description": "Options that affect how output is generated.", + "properties": { + "culture": { + "title": "Culture", + "description": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used.", + "markdownDescription": "One or more cultures to use for generating output. When multiple cultures are specified, the first matching culture will be used. By default, the current PowerShell culture is used. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#outputculture)", + "oneOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "A culture for generating output.", + "minLength": 2, + "uniqueItems": true + } + }, + { + "type": "string", + "minLength": 2, + "defaultSnippets": [ + { + "label": "en-AU", + "bodyText": "en-AU" + }, + { + "label": "en-US", + "bodyText": "en-US" + }, + { + "label": "en-GB", + "bodyText": "en-GB" + } + ] + } + ] + }, + "path": { + "type": "string", + "title": "Output path", + "description": "The file path location to save results.", + "markdownDescription": "The file path location to save results. [See help](https://github.com/microsoft/PSDocs/blob/main/docs/concepts/PSDocs/en-US/about_PSDocs_Options.md#outputpath)" + } + }, + "additionalProperties": false + }, + "options": { + "properties": { + "configuration": { + "type": "object", + "$ref": "#/definitions/configuration" + }, + "execution": { + "type": "object", + "$ref": "#/definitions/execution-option" + }, + "input": { + "type": "object", + "$ref": "#/definitions/input-option" + }, + "markdown": { + "type": "object", + "$ref": "#/definitions/markdown-option" + }, + "output": { + "type": "object", + "$ref": "#/definitions/output-option" + } + }, + "additionalProperties": false + } + } +} diff --git a/packages/psdocs/scripts/dependencies.psm1 b/packages/psdocs/scripts/dependencies.psm1 new file mode 100644 index 00000000..5e230e8d --- /dev/null +++ b/packages/psdocs/scripts/dependencies.psm1 @@ -0,0 +1,152 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Note: +# Handles dependencies updates. + +function Update-Dependencies { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $False)] + [String]$Path = (Join-Path -Path $PWD -ChildPath 'modules.json'), + + [Parameter(Mandatory = $False)] + [String]$Repository = 'PSGallery' + ) + process { + $modules = Get-Content -Path $Path -Raw | ConvertFrom-Json -AsHashtable; + $dependencies = CheckVersion $modules.dependencies -Repository $Repository; + $devDependencies = CheckVersion $modules.devDependencies -Repository $Repository -Dev; + + $modules = [Ordered]@{ + dependencies = $dependencies + devDependencies = $devDependencies + } + $modules | ConvertTo-Json -Depth 10 | Set-Content -Path $Path; + + $updates = @(git status --porcelain); + if ($Null -ne $Env:WORKING_BRANCH -and $Null -ne $updates -and $updates.Length -gt 0) { + git add modules.json; + git commit -m "Update $path"; + git push --force -u origin $Env:WORKING_BRANCH; + + $existingBranch = @(gh pr list --head $Env:WORKING_BRANCH --state open --json number | ConvertFrom-Json); + if ($Null -eq $existingBranch -or $existingBranch.Length -eq 0) { + gh pr create -B 'main' -H $Env:WORKING_BRANCH -l 'dependencies' -t 'Bump PowerShell dependencies' -F 'out/updates.txt'; + } + else { + $pr = $existingBranch[0].number + gh pr edit $pr -F 'out/updates.txt'; + } + } + } +} + +function Install-Dependencies { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $False)] + [String]$Path = (Join-Path -Path $PWD -ChildPath 'modules.json'), + + [Parameter(Mandatory = $False)] + [String]$Repository = 'PSGallery', + + [Parameter(Mandatory = $False)] + [Switch]$Dev + ) + process { + $modules = Get-Content -Path $Path -Raw | ConvertFrom-Json; + InstallVersion $modules.dependencies -Repository $Repository; + if ($Dev) { + InstallVersion $modules.devDependencies -Repository $Repository -Dev; + } + } +} + +function CheckVersion { + [CmdletBinding()] + [OutputType([System.Collections.Specialized.OrderedDictionary])] + param ( + [Parameter(Mandatory = $True)] + [Hashtable]$InputObject, + + [Parameter(Mandatory = $True)] + [String]$Repository, + + [Parameter(Mandatory = $False)] + [Switch]$Dev, + + [Parameter(Mandatory = $False)] + [String]$OutputPath = 'out/' + ) + begin { + $group = 'Dependencies'; + if ($Dev) { + $group = 'DevDependencies'; + } + if (!(Test-Path -Path $OutputPath)) { + $Null = New-Item -Path $OutputPath -ItemType Directory -Force; + } + $changeNotes = Join-Path -Path $OutputPath -ChildPath 'updates.txt'; + } + process { + $dependencies = [Ordered]@{ }; + $InputObject.GetEnumerator() | Sort-Object -Property Name | ForEach-Object { + $dependencies[$_.Name] = $_.Value + } + foreach ($module in $dependencies.GetEnumerator()) { + Write-Host -Object "[$group] -- Checking $($module.Name)"; + $installParams = @{} + $installParams += $module.Value; + $installParams.MinimumVersion = $installParams.version; + $installParams.Remove('version'); + $available = @(Find-Module -Repository $Repository -Name $module.Name @installParams -ErrorAction Ignore); + foreach ($found in $available) { + if (([Version]$found.Version) -gt ([Version]$module.Value.version)) { + Write-Host -Object "[$group] -- Newer version found $($found.Version)"; + $dependencies[$module.Name].version = $found.Version; + $Null = Add-Content -Path $changeNotes -Value "Bump $($module.Name) to v$($found.Version)."; + } + else { + Write-Host -Object "[$group] -- Already up to date."; + } + } + } + return $dependencies; + } +} + +function InstallVersion { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $True)] + [PSObject]$InputObject, + + [Parameter(Mandatory = $True)] + [String]$Repository, + + [Parameter(Mandatory = $False)] + [Switch]$Dev + ) + begin { + $group = 'Dependencies'; + if ($Dev) { + $group = 'DevDependencies'; + } + } + process { + foreach ($module in $InputObject.PSObject.Properties.GetEnumerator()) { + Write-Host -Object "[$group] -- Installing $($module.Name) v$($module.Value.version)"; + $installParams = @{ RequiredVersion = $module.Value.version }; + if ($Null -eq (Get-InstalledModule -Name $module.Name @installParams -ErrorAction Ignore)) { + Install-Module -Name $module.Name @installParams -Force -Repository $Repository; + } + } + } +} + +Export-ModuleMember -Function @( + 'Update-Dependencies' + 'Install-Dependencies' +) diff --git a/packages/psdocs/scripts/pipeline-deps.ps1 b/packages/psdocs/scripts/pipeline-deps.ps1 new file mode 100644 index 00000000..29f37a37 --- /dev/null +++ b/packages/psdocs/scripts/pipeline-deps.ps1 @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Install dependencies for integration with Azure DevOps +# + +if ($Env:SYSTEM_DEBUG -eq 'true') { + $VerbosePreference = 'Continue'; +} + +if ($Null -eq (Get-PackageProvider -Name NuGet -ErrorAction Ignore)) { + Install-PackageProvider -Name NuGet -Force -Scope CurrentUser; +} + +if ($Null -eq (Get-InstalledModule -Name PowerShellGet -MinimumVersion 2.1.2 -ErrorAction Ignore)) { + Install-Module PowerShellGet -MinimumVersion 2.1.2 -Scope CurrentUser -Force -AllowClobber; +} + +if ($Null -eq (Get-InstalledModule -Name InvokeBuild -MinimumVersion 5.4.0 -ErrorAction Ignore)) { + Install-Module InvokeBuild -MinimumVersion 5.4.0 -Scope CurrentUser -Force; +} diff --git a/packages/psdocs/src/PSDocs.Benchmark/Benchmark.Doc.ps1 b/packages/psdocs/src/PSDocs.Benchmark/Benchmark.Doc.ps1 new file mode 100644 index 00000000..8c302a76 --- /dev/null +++ b/packages/psdocs/src/PSDocs.Benchmark/Benchmark.Doc.ps1 @@ -0,0 +1,150 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Document defintions for benchmarks +# + +#region BlockQuote + +document 'BlockQuoteSingleMarkdown' { + 'This is a single line' | BlockQuote +} + +document 'BlockQuoteMultiMarkdown' { + @('This is the first line.' + 'This is the second line.') | BlockQuote +} + +document 'BlockQuoteTitleMarkdown' { + 'This is a single block quote' | BlockQuote -Title 'Test' +} + +document 'BlockQuoteInfoMarkdown' { + 'This is a single block quote' | BlockQuote -Info 'Tip' +} + +#endregion BlockQuote + +#region Code + +document 'CodeMarkdown' { + Code { + # This is a comment + This is code + + # Another comment + And code + } +} + +document 'CodeMarkdownNamedFormat' { + Code powershell { + Get-Content + } +} + +document 'CodeMarkdownEval' { + $a = 1; $a += 1; $a | Code powershell; +} + +#endregion Code + +#region Include + +#endregion Include + +#region Metadata + +document 'MetadataSingleEntry' { + Metadata ([ordered]@{ + title = 'Test' + }) +} + +document 'MetadataMultipleEntry' { + Metadata ([ordered]@{ + value1 = 'ABC' + value2 = 'EFG' + }) +} + +document 'MetadataMultipleBlock' { + Metadata ([ordered]@{ + value1 = 'ABC' + }) + Section 'Test' { + 'A test section spliting metadata blocks.' + } + Metadata @{ + value2 = 'EFG' + } +} + +document 'NoMetdata' { + Section 'Test' { + 'A test section.' + } +} + +document 'NullMetdata' { + Metadata $Null + Section 'Test' { + 'A test section.' + } +} + +#endregion Metadata + +#region Note + +document 'NoteSingleMarkdown' { + 'This is a single line' | Note +} + +document 'NoteMultiMarkdown' { + @('This is the first line.' + 'This is the second line.') | Note +} + +document 'NoteScriptBlockMarkdown' { + Note { + 'This is a single line' + } +} + +#endregion Note + +#region Section + +document 'SectionBlockTests' { + Section 'SingleLine' { + 'This is a single line markdown section.' + } + Section 'MultiLine' { + "This is a multiline`r`ntest." + } + Section 'Empty' { + } + Section 'Forced' -Force { + } +} + +document 'SectionWhen' { + Section 'Section 1' -If { $False } { + 'Content 1' + } + Section 'Section 2' -If { $True } { + 'Content 2' + } + # Support for When alias of If + Section 'Section 3' -When { $True } { + 'Content 3' + } +} + +#endregion Section + +#region Table + +#endregion Table diff --git a/packages/psdocs/src/PSDocs.Benchmark/PSDocs.Benchmark.csproj b/packages/psdocs/src/PSDocs.Benchmark/PSDocs.Benchmark.csproj new file mode 100644 index 00000000..caf39b71 --- /dev/null +++ b/packages/psdocs/src/PSDocs.Benchmark/PSDocs.Benchmark.csproj @@ -0,0 +1,45 @@ + + + + Exe + net7.0 + AnyCPU + {a9f68ba6-1381-4e35-a619-a948d47ec752} + + + + TRACE;BENCHMARK + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/packages/psdocs/src/PSDocs.Benchmark/PSDocs.cs b/packages/psdocs/src/PSDocs.Benchmark/PSDocs.cs new file mode 100644 index 00000000..8d547b00 --- /dev/null +++ b/packages/psdocs/src/PSDocs.Benchmark/PSDocs.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Management.Automation; +using System.Reflection; +using BenchmarkDotNet.Attributes; +using PSDocs.Configuration; +using PSDocs.Models; +using PSDocs.Pipeline; +using PSDocs.Processor.Markdown; +using PSDocs.Runtime; + +namespace PSDocs.Benchmark +{ + /// + /// Define a set of benchmarks for performance testing PSDocs internals. + /// + [MemoryDiagnoser] + [MarkdownExporterAttribute.GitHub] + public class PSDocs + { + private Document[] _Document; + private PSObject _SourceObject; + private Action _InvokeMarkdownProcessor; + private Action _InvokePipeline; + + [GlobalSetup] + public void Prepare() + { + PrepareMarkdownProcessor(); + PrepareInvokePipeline(); + PrepareDocument(); + PrepareSourceObject(); + } + + private void PrepareMarkdownProcessor() + { + var option = GetOption(); + var processor = GetProcessor(); + _InvokeMarkdownProcessor = (document) => processor.Process(option, document); + } + + private void PrepareInvokePipeline() + { + var option = GetOption(); + var builder = PipelineBuilder.Invoke(GetSource(), option, null, null); + var pipeline = builder.Build(); + _InvokePipeline = pipeline.Process; + } + + private void PrepareDocument() + { + _Document = new Document[] + { + GetDocument() + }; + } + + private static Document GetDocument() + { + var context = new DocumentContext(null) + { + InstanceName = "test-benchmark" + }; + var result = new Document(context) + { + Title = "Test document" + }; + var section = new Section + { + Title = "Section 1", + Level = 2 + }; + result.Node.Add(section); + return result; + } + + private void PrepareSourceObject() + { + _SourceObject = PSObject.AsPSObject("Test"); + } + + private static MarkdownProcessor GetProcessor() + { + return new MarkdownProcessor(); + } + + private static PSDocumentOption GetOption() + { + return new PSDocumentOption(); + } + + private static Source[] GetSource() + { + return new Source[] { + new Source(GetSourcePath(), new SourceFile[] { new SourceFile(GetSourcePath("Benchmark.Doc.ps1"), null, SourceType.Script, null) }) + }; + } + + private static string GetSourcePath() + { + return Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); + } + + private static string GetSourcePath(string fileName) + { + return Path.Combine(GetSourcePath(), fileName); + } + + [Benchmark] + public void InvokeMarkdownProcessor() + { + _InvokeMarkdownProcessor(_Document[0]); + } + + [Benchmark] + public void InvokePipeline() + { + _InvokePipeline(_SourceObject); + } + } +} diff --git a/packages/psdocs/src/PSDocs.Benchmark/Program.cs b/packages/psdocs/src/PSDocs.Benchmark/Program.cs new file mode 100644 index 00000000..8d74d5bb --- /dev/null +++ b/packages/psdocs/src/PSDocs.Benchmark/Program.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; +using Microsoft.Extensions.CommandLineUtils; + +namespace PSDocs.Benchmark +{ + internal static class Program + { + private static void Main(string[] args) + { + var app = new CommandLineApplication + { + Name = "PSDocs Benchmark", + Description = "A runner for testing PSDocs performance" + }; + +#if !BENCHMARK + // Do profiling + DebugProfile(); +#endif + +#if BENCHMARK + RunProfile(app); + app.Execute(args); +#endif + } + + private static void RunProfile(CommandLineApplication app) + { + var config = ManualConfig.CreateEmpty() + .AddLogger(ConsoleLogger.Default) + .AddColumnProvider(DefaultColumnProviders.Instance) + .AddAnalyser(EnvironmentAnalyser.Default) + .AddAnalyser(OutliersAnalyser.Default) + .AddAnalyser(MinIterationTimeAnalyser.Default) + .AddAnalyser(MultimodalDistributionAnalyzer.Default) + .AddAnalyser(RuntimeErrorAnalyser.Default) + .AddAnalyser(ZeroMeasurementAnalyser.Default); + + app.Command("benchmark", cmd => + { + var output = cmd.Option("-o | --output", "The path to store report output.", CommandOptionType.SingleValue); + + cmd.OnExecute(() => + { + if (output.HasValue()) + { + config.WithArtifactsPath(output.Value()); + } + + // Do benchmarks + BenchmarkRunner.Run(config); + + return 0; + }); + + cmd.HelpOption("-? | -h | --help"); + }); + + app.HelpOption("-? | -h | --help"); + } + + private static void DebugProfile() + { + Thread.Sleep(2000); + var profile = new PSDocs(); + profile.Prepare(); + + for (var i = 0; i < 1000; i++) + { + profile.InvokeMarkdownProcessor(); + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Annotations/CommentMetadata.cs b/packages/psdocs/src/PSDocs/Annotations/CommentMetadata.cs new file mode 100644 index 00000000..b41a467b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Annotations/CommentMetadata.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace PSDocs.Annotations +{ + /// + /// Metadata properties that can be exposed by comment help. + /// + [DebuggerDisplay("Synopsis = {Synopsis}")] + internal sealed class CommentMetadata + { + public string Synopsis; + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/BlockQuoteCommand.cs b/packages/psdocs/src/PSDocs/Commands/BlockQuoteCommand.cs new file mode 100644 index 00000000..1191608a --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/BlockQuoteCommand.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Management.Automation; +using PSDocs.Models; + +namespace PSDocs.Commands +{ + internal abstract class BlockQuoteCommandBase : KeywordCmdlet + { + private List _Content; + + #region Properties + + [Parameter()] + public string Title { get; set; } + + [Parameter(Position = 0, Mandatory = true, ValueFromPipeline = true)] + public string Text { get; set; } + + #endregion Properties + + protected override void BeginProcessing() + { + _Content = new List(); + } + + protected override void ProcessRecord() + { + _Content.Add(Text); + } + + protected override void EndProcessing() + { + try + { + var node = ModelHelper.BlockQuote(GetInfo(), Title); + node.Content = _Content.ToArray(); + WriteObject(node); + } + finally + { + _Content.Clear(); + } + } + + protected abstract string GetInfo(); + } + + [Cmdlet(VerbsCommon.Format, LanguageKeywords.BlockQuote)] + internal sealed class BlockQuoteCommand : BlockQuoteCommandBase + { + [Parameter()] + public string Info { get; set; } + + protected override string GetInfo() + { + return Info; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/CodeCommand.cs b/packages/psdocs/src/PSDocs/Commands/CodeCommand.cs new file mode 100644 index 00000000..24476242 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/CodeCommand.cs @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using Newtonsoft.Json; +using PSDocs.Models; +using PSDocs.Runtime; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Format, LanguageKeywords.Code)] + internal sealed class CodeCommand : KeywordCmdlet + { + private const string ParameterSet_Pipeline = "Pipeline"; + private const string ParameterSet_InfoString = "InfoString"; + private const string ParameterSet_PipelineInfoString = "PipelineInfoString"; + private const string ParameterSet_Default = "Default"; + + private const string Info_ScriptBlock = "powershell"; + + private List _Content; + + [Parameter(Position = 0, Mandatory = true, ParameterSetName = ParameterSet_InfoString)] + [Parameter(Position = 0, Mandatory = true, ParameterSetName = ParameterSet_PipelineInfoString)] + public string Info { get; set; } + + [Parameter(Position = 0, Mandatory = true, ParameterSetName = ParameterSet_Default, ValueFromPipeline = false)] + [Parameter(Position = 1, Mandatory = true, ParameterSetName = ParameterSet_InfoString, ValueFromPipeline = false)] + public ScriptBlock Body { get; set; } + + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_Pipeline, ValueFromPipeline = true)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_PipelineInfoString, ValueFromPipeline = true)] + [AllowNull] + [AllowEmptyString] + [AllowEmptyCollection] + public object InputObject { get; set; } + + protected override void BeginProcessing() + { + _Content = new List(); + } + + protected override void ProcessRecord() + { + var content = ParameterSetName == ParameterSet_Pipeline || ParameterSetName == ParameterSet_PipelineInfoString ? InputObject : Body; + AddContent(content); + } + + protected override void EndProcessing() + { + try + { + var node = ModelHelper.NewCode(); + node.Info = Info; + node.Content = string.Join(Environment.NewLine, _Content.ToArray()); + WriteObject(node); + } + finally + { + _Content.Clear(); + } + } + + private void AddContent(object input) + { + var result = TryConvertScriptBlock(input, Info, out var content) || + TryConvertJson(input, Info, out content) || + TryConvertYaml(input, Info, out content) || + TryConvertString(input, Info, out content); + + if (!result || content == null) + return; + + _Content.AddRange(content.ReadLines()); + Info = content.Info; + } + + private static bool TryConvertJson(object input, string info, out StringContent content) + { + content = null; + if (!IsJson(info)) + return false; + + var baseObject = ObjectHelper.GetBaseObject(input); + var baseType = baseObject.GetType(); + if (baseObject is string || baseType.IsValueType) + return false; + + using var sw = new StringWriter(); + using (var writer = new JsonTextWriter(sw)) + { + writer.Indentation = 4; + var serializer = new JsonSerializer(); + serializer.Converters.Insert(0, new PSObjectJsonConverter()); + serializer.Formatting = Formatting.Indented; + serializer.Serialize(writer, input); + } + content = new StringContent(sw.ToString(), info); + return true; + } + + private static bool TryConvertYaml(object input, string info, out StringContent content) + { + content = null; + if (!IsYaml(info)) + return false; + + var baseObject = ObjectHelper.GetBaseObject(input); + var baseType = baseObject.GetType(); + if (baseObject is string || baseObject is Include || baseType.IsValueType) + return false; + + var s = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeInspector(inspector => new PSObjectTypeInspector(inspector)) + .Build(); + + content = new StringContent(s.Serialize(input), info); + return true; + } + + private static bool TryConvertScriptBlock(object input, string info, out StringContent content) + { + content = null; + if (input is not ScriptBlock s) + return false; + + content = new StringContent(s.ToString(), info ?? Info_ScriptBlock); + return true; + } + + private static bool TryConvertString(object input, string info, out StringContent content) + { + content = null; + if (input == null) + return false; + + content = new StringContent(input.ToString(), info); + return true; + } + + private static bool IsYaml(string info) + { + return StringComparer.OrdinalIgnoreCase.Equals("yaml", info) || + StringComparer.OrdinalIgnoreCase.Equals("yml", info); + } + + private static bool IsJson(string info) + { + return StringComparer.OrdinalIgnoreCase.Equals("json", info); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/DefinitionCommand.cs b/packages/psdocs/src/PSDocs/Commands/DefinitionCommand.cs new file mode 100644 index 00000000..f4b0adae --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/DefinitionCommand.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSDocs.Runtime; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.New, LanguageKeywords.Definition)] + internal sealed class DefinitionCommand : PSCmdlet + { + private const string InvokeCmdletName = "Invoke-Block"; + private const string InvokeCmdlet_IfParameter = "If"; + private const string InvokeCmdlet_WithParameter = "With"; + private const string InvokeCmdlet_BodyParameter = "Body"; + + /// + /// Document block name. + /// + [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty()] + public string Name { get; set; } + + /// + /// Document block body. + /// + [Parameter(Mandatory = true, Position = 1)] + public ScriptBlock Body { get; set; } + + /// + /// Document block tags. + /// + [Parameter(Mandatory = false)] + public string[] Tag { get; set; } + + /// + /// An optional script precondition before the Document is evaluated. + /// + [Parameter(Mandatory = false)] + public ScriptBlock If { get; set; } + + /// + /// An optional selector precondition before the Document is evaluated. + /// + [Parameter(Mandatory = false)] + public string[] With { get; set; } + + protected override void ProcessRecord() + { + var context = RunspaceContext.CurrentThread; + var source = context.Source; + var extent = new ResourceExtent( + file: source.File.Path, + startLineNumber: Body.Ast.Extent.StartLineNumber + ); + + // Create PS instance for execution + var ps = context.NewPowerShell(); + ps.AddCommand(new CmdletInfo(InvokeCmdletName, typeof(InvokeDocumentCommand))); + ps.AddParameter(InvokeCmdlet_IfParameter, If); + ps.AddParameter(InvokeCmdlet_WithParameter, With); + ps.AddParameter(InvokeCmdlet_BodyParameter, Body); + + var block = new ScriptDocumentBlock( + source: source.File, + name: Name, + body: ps, + tag: Tag, + extent: extent + ); + WriteObject(block); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/ExportConventionCommand.cs b/packages/psdocs/src/PSDocs/Commands/ExportConventionCommand.cs new file mode 100644 index 00000000..952d4fed --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/ExportConventionCommand.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSDocs.Definitions.Conventions; +using PSDocs.Runtime; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsLifecycle.Register, LanguageKeywords.Convention)] + internal sealed class ExportConventionCommand : PSCmdlet + { + private const string InvokeCmdletName = "Invoke-PSDocumentConvention"; + private const string InvokeCmdlet_BodyParameter = "Body"; + + /// + /// Convention name. + /// + [Parameter(Mandatory = false, Position = 0)] + [ValidateNotNullOrEmpty()] + public string Name { get; set; } + + /// + /// Begin block. + /// + [Parameter(Mandatory = false)] + public ScriptBlock Begin { get; set; } + + /// + /// Process block. + /// + [Parameter(Mandatory = false, Position = 1)] + public ScriptBlock Process { get; set; } + + /// + /// End block. + /// + [Parameter(Mandatory = false)] + public ScriptBlock End { get; set; } + + protected override void ProcessRecord() + { + var context = RunspaceContext.CurrentThread; + var source = context.Source; + + // Return convention + var result = new ScriptBlockDocumentConvention( + source: source.File, + name: Name, + begin: ConventionBlock(context, Begin), + process: ConventionBlock(context, Process), + end: ConventionBlock(context, End) + ); + WriteObject(result); + } + + private static LanguageScriptBlock ConventionBlock(RunspaceContext context, ScriptBlock block) + { + if (block == null) + return null; + + // Create PS instance for execution + var ps = context.NewPowerShell(); + ps.AddCommand(new CmdletInfo(InvokeCmdletName, typeof(InvokeConventionCommand))); + ps.AddParameter(InvokeCmdlet_BodyParameter, block); + return new LanguageScriptBlock(ps); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/IncludeCommand.cs b/packages/psdocs/src/PSDocs/Commands/IncludeCommand.cs new file mode 100644 index 00000000..152a21e0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/IncludeCommand.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.IO; +using System.Management.Automation; +using PSDocs.Models; +using PSDocs.Resources; +using PSDocs.Runtime; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Add, LanguageKeywords.Include)] + [OutputType(typeof(Include))] + internal sealed class IncludeCommand : KeywordCmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string FileName { get; set; } + + [Parameter(Mandatory = false)] + public string BaseDirectory { get; set; } + + [Parameter(Mandatory = false)] + public string Culture { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter UseCulture { get; set; } + + [Parameter(Mandatory = false)] + public IDictionary Replace { get; set; } + + protected override void BeginProcessing() + { + if (string.IsNullOrEmpty(Culture)) + Culture = RunspaceContext.CurrentThread.Culture; + } + + protected override void EndProcessing() + { + var result = ModelHelper.Include(BaseDirectory, Culture, FileName, UseCulture, Replace); + if (result == null || !result.Exists) + { + WriteError(new ErrorRecord( + exception: new FileNotFoundException(PSDocsResources.IncludeNotFound, result?.Path), + errorId: "PSDocs.Runtime.IncludeNotFound", + errorCategory: ErrorCategory.ObjectNotFound, + targetObject: result?.Path + )); + return; + } + WriteObject(result); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/InvokeConventionCommand.cs b/packages/psdocs/src/PSDocs/Commands/InvokeConventionCommand.cs new file mode 100644 index 00000000..3d1b3b65 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/InvokeConventionCommand.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsLifecycle.Invoke, LanguageKeywords.Convention)] + internal sealed class InvokeConventionCommand : PSCmdlet + { + [Parameter()] + public string[] Type; + + [Parameter()] + public ScriptBlock If; + + [Parameter()] + public ScriptBlock Body; + + [Parameter(ValueFromPipeline = true)] + public PSObject InputObject; + + protected override void ProcessRecord() + { + try + { + if (Body == null) + return; + + WriteObject(Body.Invoke(InputObject), true); + } + finally + { + + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/InvokeDocumentCommand.cs b/packages/psdocs/src/PSDocs/Commands/InvokeDocumentCommand.cs new file mode 100644 index 00000000..5abcc6ab --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/InvokeDocumentCommand.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using System.Management.Automation; +using PSDocs.Runtime; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsLifecycle.Invoke, LanguageKeywords.Block)] + internal sealed class InvokeDocumentCommand : PSCmdlet + { + [Parameter()] + public string[] With; + + [Parameter()] + public ScriptBlock If; + + [Parameter()] + public ScriptBlock Body; + + protected override void ProcessRecord() + { + try + { + if (Body == null) + return; + + // Evalute selector pre-condition + if (!AcceptsWith()) + return; + + // Evaluate script pre-condition + if (!AcceptsIf()) + return; + + WriteObject(Body.Invoke(), true); + } + finally + { + + } + } + + private bool AcceptsWith() + { + if (With == null || With.Length == 0) + return true; + + for (var i = 0; i < With.Length; i++) + { + if (RunspaceContext.CurrentThread.TrySelector(With[i])) + return true; + } + return false; + } + + private bool AcceptsIf() + { + if (If == null) + return true; + + try + { + RunspaceContext.CurrentThread.PushScope(RunspaceScope.Condition); + return GetResult(If.Invoke()); + } + finally + { + RunspaceContext.CurrentThread.PopScope(); + } + } + + private static bool GetResult(Collection result) + { + if (result == null || result.Count == 0) + return false; + + for (var i = 0; i < result.Count; i++) + { + if (result[i] == null || result[i].BaseObject == null) + return false; + + if (result[i].BaseObject is bool bResult && !bResult) + return false; + } + return true; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/Keyword.cs b/packages/psdocs/src/PSDocs/Commands/Keyword.cs new file mode 100644 index 00000000..e8dc17c6 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/Keyword.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSDocs.Data.Internal; +using PSDocs.Pipeline; +using PSDocs.Runtime; + +namespace PSDocs.Commands +{ + internal static class LanguageKeywords + { + public const string Definition = "Definition"; + public const string Convention = "PSDocumentConvention"; + public const string Document = "Document"; + public const string Block = "Block"; + public const string Note = "Note"; + public const string Warning = "Warning"; + public const string Code = "Code"; + public const string BlockQuote = "BlockQuote"; + public const string Table = "Table"; + public const string List = "List"; + public const string Include = "Include"; + public const string Section = "Section"; + public const string Metadata = "Metadata"; + public const string Title = "Title"; + } + + internal static class LanguageCmdlets + { + public const string NewDefinition = "New-Definition"; + public const string ExportConvention = "Export-PSDocumentConvention"; + public const string NewSection = "New-Section"; + public const string InvokeBlock = "Invoke-Block"; + public const string InvokeConvention = "Invoke-PSDocumentConvention"; + public const string FormatCode = "Format-Code"; + public const string FormatBlockQuote = "Format-BlockQuote"; + public const string FormatNote = "Format-Note"; + public const string FormatWarning = "Format-Warning"; + public const string FormatTable = "Format-Table"; + public const string FormatList = "Format-List"; + public const string SetMetadata = "Set-Metadata"; + public const string SetTitle = "Set-Title"; + public const string AddInclude = "Add-Include"; + } + + internal abstract class KeywordCmdlet : PSCmdlet + { + protected static ScriptDocumentBuilder Builder => RunspaceContext.CurrentThread.Builder; + + protected static ScriptDocumentBuilder GetBuilder() + { + return RunspaceContext.CurrentThread.Builder; + } + + protected static PSObject GetTargetObject() + { + return RunspaceContext.CurrentThread.TargetObject.Value; + } + + protected static PipelineContext GetPipeline() + { + return RunspaceContext.CurrentThread.Pipeline; + } + + protected static bool True(object o) + { + return o != null && TryBool(o, out var bResult) && bResult; + } + + protected static bool TryBool(object o, out bool value) + { + value = false; + if (ObjectHelper.GetBaseObject(o) is bool result) + { + value = result; + return true; + } + return false; + } + + protected static bool TryString(object o, out string value) + { + value = null; + if (ObjectHelper.GetBaseObject(o) is string result) + { + value = result; + return true; + } + return false; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/ListCommand.cs b/packages/psdocs/src/PSDocs/Commands/ListCommand.cs new file mode 100644 index 00000000..6192a508 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/ListCommand.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Format, LanguageKeywords.List)] + internal sealed class ListCommand : KeywordCmdlet + { + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/MetadataCommand.cs b/packages/psdocs/src/PSDocs/Commands/MetadataCommand.cs new file mode 100644 index 00000000..78af5623 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/MetadataCommand.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Set, LanguageKeywords.Metadata)] + internal sealed class MetadataCommand : KeywordCmdlet + { + [Parameter(Position = 0)] + public IDictionary Body { get; set; } + + protected override void BeginProcessing() + { + if (Body == null || Body.Count == 0) + return; + + GetBuilder().Metadata(Body); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/NoteCommand.cs b/packages/psdocs/src/PSDocs/Commands/NoteCommand.cs new file mode 100644 index 00000000..4fe583bf --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/NoteCommand.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Format, LanguageKeywords.Note)] + internal sealed class NoteCommand : BlockQuoteCommandBase + { + private const string InfoString = "NOTE"; + + protected override string GetInfo() + { + return InfoString; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/SectionCommand.cs b/packages/psdocs/src/PSDocs/Commands/SectionCommand.cs new file mode 100644 index 00000000..e161d168 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/SectionCommand.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.New, LanguageKeywords.Section)] + internal sealed class SectionCommand : KeywordCmdlet + { + [Parameter(Position = 0, Mandatory = true)] + public string Name { get; set; } + + [Parameter(Position = 1, Mandatory = true)] + public ScriptBlock Body { get; set; } + + [Parameter(Mandatory = false)] + public ScriptBlock If { get; set; } + + [Parameter(Mandatory = false)] + public SwitchParameter Force { get; set; } + + [Parameter(Mandatory = false, ValueFromPipeline = true)] + public PSObject InputObject { get; set; } + + protected override void ProcessRecord() + { + if (!TryCondition()) + return; + + var builder = GetBuilder(); + var section = builder.EnterSection(Name); + var shouldWrite = true; + + try + { + shouldWrite = section.AddNodes(Body.Invoke()) || ShouldForce(); + } + finally + { + builder.ExitSection(); + } + if (shouldWrite) + WriteObject(section); + } + + private bool ShouldForce() + { + return Force.ToBool() || !GetPipeline().Option.Markdown.SkipEmptySections.Value; + } + + private bool TryCondition() + { + return If == null || True(If.InvokeReturnAsIs()); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/TableCommand.cs b/packages/psdocs/src/PSDocs/Commands/TableCommand.cs new file mode 100644 index 00000000..7714ccd2 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/TableCommand.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; +using PSDocs.Models; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Format, LanguageKeywords.Table)] + internal sealed class TableCommand : KeywordCmdlet + { + private const string ObjectKey_Label = "label"; + private const string ObjectKey_Name = "name"; + + private TableBuilder _TableBuilder; + private List _RowData; + private PropertyReader _Reader; + + internal sealed class PropertyReader + { + private readonly Dictionary _Map; + private readonly List _Properties; + + public PropertyReader() + { + _Map = new Dictionary(); + _Properties = new List(); + } + + public void Add(string propertyName, GetProperty get) + { + if (string.IsNullOrEmpty(propertyName) || _Map.ContainsKey(propertyName)) + return; + + _Map.Add(propertyName, get); + _Properties.Add(propertyName); + } + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < _Properties.Count; i++) + { + yield return _Map[_Properties[i]]; + } + } + } + + internal delegate string GetProperty(PSObject value); + + [Parameter(ValueFromPipeline = true)] + public PSObject InputObject { get; set; } + + [Parameter(Position = 0)] + public object[] Property { get; set; } + + protected override void BeginProcessing() + { + _TableBuilder = ModelHelper.Table(); + _RowData = new List(); + BuildReader(); + } + + private void BuildReader() + { + _Reader = new PropertyReader(); + if (Property == null || Property.Length == 0) + return; + + for (var i = 0; i < Property.Length; i++) + { + if (Property[i] is Hashtable propertyExpression) + { + _TableBuilder.Header(propertyExpression); + if (propertyExpression["Expression"] is ScriptBlock expression) + { + var propertyName = GetPropertyName(propertyExpression); + _Reader.Add(propertyName, (PSObject value) => ReadPropertyByExpression(value, expression)); + } + else + { + var propertyName = propertyExpression["Expression"].ToString(); + _Reader.Add(propertyName, (PSObject value) => ReadPropertyByName(value, propertyName)); + } + } + else + { + var propertyName = Property[i].ToString(); + _TableBuilder.Header(propertyName); + _Reader.Add(propertyName, (PSObject value) => ReadPropertyByName(value, propertyName)); + } + } + } + + protected override void ProcessRecord() + { + if (Property == null || Property.Length == 0) + { + // Extract out the header column names based on the resulting objects + foreach (var property in InputObject.Properties) + { + var propertyName = property.Name.ToString(); + _TableBuilder.Header(propertyName); + _Reader.Add(propertyName, (PSObject value) => ReadPropertyByName(value, propertyName)); + } + } + _RowData.Add(InputObject); + } + + protected override void EndProcessing() + { + var table = _TableBuilder.Build(); + var rows = new List(); + + for (var i = 0; i < _RowData.Count; i++) + rows.Add(ReadFields(_RowData[i])); + + table.Rows = rows; + if (table.Rows.Count > 0) + WriteObject(table); + } + + private static string ReadPropertyByName(PSObject value, string propertyName) + { + return value.Properties[propertyName]?.Value?.ToString(); + } + + private static string ReadPropertyByExpression(PSObject value, ScriptBlock expression) + { + var variables = new List(new PSVariable[] { new PSVariable("_", value) }); + var output = GetPSObject(expression.InvokeWithContext(null, variables, null)); + return TryString(output, out var soutput) ? soutput : output.ToString(); + } + + private static PSObject GetPSObject(Collection collection) + { + if (collection == null || collection.Count == 0) + return null; + + return collection[0]; + } + + private static string GetPropertyName(Hashtable propertyExpression) + { + if (propertyExpression.ContainsKey(ObjectKey_Label)) + return propertyExpression[ObjectKey_Label].ToString(); + + if (propertyExpression.ContainsKey(ObjectKey_Name)) + return propertyExpression[ObjectKey_Name].ToString(); + + return null; + } + + private string[] ReadFields(PSObject row) + { + if (row == null) + return Array.Empty(); + + var fields = new List(); + foreach (var getter in _Reader) + fields.Add(getter.Invoke(row)); + + return fields.ToArray(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/TitleCommand.cs b/packages/psdocs/src/PSDocs/Commands/TitleCommand.cs new file mode 100644 index 00000000..1f216650 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/TitleCommand.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSDocs.Pipeline; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Set, LanguageKeywords.Title)] + internal sealed class TitleCommand : KeywordCmdlet + { + [Parameter(Position = 0, Mandatory = true)] + [AllowNull, AllowEmptyString] + public string Text { get; set; } + + protected override void BeginProcessing() + { + if (string.IsNullOrEmpty(Text)) + { + GetPipeline().Writer.WarnTitleEmpty(); + return; + } + GetBuilder().Title(Text); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Commands/WarningCommand.cs b/packages/psdocs/src/PSDocs/Commands/WarningCommand.cs new file mode 100644 index 00000000..e16f2ea9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Commands/WarningCommand.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Commands +{ + [Cmdlet(VerbsCommon.Format, LanguageKeywords.Warning)] + internal sealed class WarningCommand : BlockQuoteCommandBase + { + private const string InfoString = "WARNING"; + + protected override string GetInfo() + { + return InfoString; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/DictionaryExtensions.cs b/packages/psdocs/src/PSDocs/Common/DictionaryExtensions.cs new file mode 100644 index 00000000..8fd04077 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/DictionaryExtensions.cs @@ -0,0 +1,142 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace PSDocs +{ + internal static class DictionaryExtensions + { + [DebuggerStepThrough] + public static bool TryPopValue(this IDictionary dictionary, string key, out object value) + { + return dictionary.TryGetValue(key, out value) && dictionary.Remove(key); + } + + [DebuggerStepThrough] + public static bool TryPopValue(this IDictionary dictionary, string key, out T value) + { + value = default; + if (TryPopValue(dictionary, key, out var v) && ObjectHelper.GetBaseObject(v) is T result) + { + value = result; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryPopBool(this IDictionary dictionary, string key, out bool value) + { + value = default; + return TryPopValue(dictionary, key, out var v) && bool.TryParse(v.ToString(), out value); + } + + [DebuggerStepThrough] + public static bool TryPopString(this IDictionary dictionary, string key, out string value) + { + return TryPopValue(dictionary, key, out value); + } + + [DebuggerStepThrough] + public static bool TryPopEnum(this IDictionary dictionary, string key, out TEnum value) where TEnum : struct + { + value = default; + return TryPopString(dictionary, key, out var result) && Enum.TryParse(result, out value); + } + + [DebuggerStepThrough] + public static bool TryPopStringArray(this IDictionary dictionary, string key, out string[] value) + { + value = null; + if (!TryPopValue(dictionary, key, out var result)) + return false; + + var o = ObjectHelper.GetBaseObject(result); + value = o.GetType().IsArray ? ((object[])o).OfType().ToArray() : new string[] { o.ToString() }; + return true; + } + + [DebuggerStepThrough] + public static bool TryGetBool(this IDictionary dictionary, string key, out bool? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is bool bvalue || (o is string svalue && bool.TryParse(svalue, out bvalue))) + { + value = bvalue; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryGetLong(this IDictionary dictionary, string key, out long? value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is long lvalue || (o is string svalue && long.TryParse(svalue, out lvalue))) + { + value = lvalue; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryGetString(this IDictionary dictionary, string key, out string value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + if (o is string sValue) + { + value = sValue; + return true; + } + return false; + } + + [DebuggerStepThrough] + public static bool TryGetStringArray(this IDictionary dictionary, string key, out string[] value) + { + value = null; + if (!dictionary.TryGetValue(key, out var o)) + return false; + + return TryStringArray(o, out value); + } + + [DebuggerStepThrough] + public static void AddUnique(this IDictionary dictionary, IEnumerable> values) + { + if (values == null) + return; + + foreach (var kv in values) + { + if (!dictionary.ContainsKey(kv.Key)) + dictionary.Add(kv.Key, kv.Value); + } + } + + [DebuggerStepThrough] + private static bool TryStringArray(object o, out string[] value) + { + value = default; + if (o == null) + return false; + + value = o.GetType().IsArray ? ((object[])o).OfType().ToArray() : new string[] { o.ToString() }; + return true; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/EnvironmentHelper.cs b/packages/psdocs/src/PSDocs/Common/EnvironmentHelper.cs new file mode 100644 index 00000000..77d70fe5 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/EnvironmentHelper.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Security; + +namespace PSDocs +{ + internal sealed class EnvironmentHelper + { + private static readonly char[] STRINGARRAY_SEPARATOR = new char[] { ';' }; + + public static readonly EnvironmentHelper Default = new(); + + internal bool TryString(string key, out string value) + { + return TryVariable(key, out value) && !string.IsNullOrEmpty(value); + } + + internal bool TrySecureString(string key, out SecureString value) + { + value = null; + if (!TryString(key, out var variable)) + return false; + + value = new NetworkCredential("na", variable).SecurePassword; + return true; + } + + internal bool TryInt(string key, out int value) + { + value = default; + return TryVariable(key, out var variable) && int.TryParse(variable, out value); + } + + internal bool TryBool(string key, out bool value) + { + value = default; + return TryVariable(key, out var variable) && TryParseBool(variable, out value); + } + + internal bool TryEnum(string key, out TEnum value) where TEnum : struct + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + return Enum.TryParse(variable, ignoreCase: true, out value); + } + + internal bool TryStringArray(string key, out string[] value) + { + value = default; + if (!TryVariable(key, out var variable)) + return false; + + value = variable.Split(STRINGARRAY_SEPARATOR, options: StringSplitOptions.RemoveEmptyEntries); + return value != null; + } + + private bool TryVariable(string key, out string variable) + { + variable = Environment.GetEnvironmentVariable(key); + return variable != null; + } + + private static bool TryParseBool(string variable, out bool value) + { + if (bool.TryParse(variable, out value)) + return true; + + if (int.TryParse(variable, out var ivalue)) + { + value = ivalue > 0; + return true; + } + return false; + } + + internal IEnumerable> WithPrefix(string prefix) + { + var env = Environment.GetEnvironmentVariables(); + var enumerator = env.GetEnumerator(); + while (enumerator.MoveNext()) + { + var key = enumerator.Key.ToString(); + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + yield return new KeyValuePair(key, enumerator.Value); + } + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/ExpressionHelpers.cs b/packages/psdocs/src/PSDocs/Common/ExpressionHelpers.cs new file mode 100644 index 00000000..d5a4e405 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/ExpressionHelpers.cs @@ -0,0 +1,470 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Text.RegularExpressions; +using System.Threading; +using Newtonsoft.Json.Linq; +using PSDocs.Configuration; +using PSDocs.Runtime; + +namespace PSDocs +{ + internal static class ExpressionHelpers + { + private const string CACHE_MATCH = "MatchRegex"; + private const string CACHE_MATCH_C = "MatchRegexCaseSensitive"; + + private const char Backslash = '\\'; + private const char Slash = '/'; + + internal static bool NullOrEmpty(object o) + { + if (o == null) + return true; + + o = GetBaseObject(o); + return (o is ICollection c && c.Count == 0) || + (TryString(o, out var s) && string.IsNullOrEmpty(s)); + } + + internal static bool Exists(IBindingContext bindingContext, object inputObject, string field, bool caseSensitive) + { + return ObjectHelper.GetField(bindingContext, inputObject, field, caseSensitive, out _); + } + + internal static bool Equal(object expectedValue, object actualValue, bool caseSensitive, bool convertExpected = false, bool convertActual = false) + { + if (TryString(expectedValue, out var s1) && TryString(actualValue, out var s2)) + return StringEqual(s1, s2, caseSensitive); + + if (TryBool(expectedValue, convertExpected, out var b1) && TryBool(actualValue, convertActual, out var b2)) + return b1 == b2; + + if (TryLong(expectedValue, convertExpected, out var l1) && TryLong(actualValue, convertActual, out var l2)) + return l1 == l2; + + if (TryInt(expectedValue, convertExpected, out var i1) && TryInt(actualValue, convertActual, out var i2)) + return i1 == i2; + + var expectedBase = GetBaseObject(expectedValue); + var actualBase = GetBaseObject(actualValue); + return expectedBase.Equals(actualBase) || expectedValue.Equals(actualValue); + } + + internal static bool CompareNumeric(object actual, object expected, bool convert, out int compare, out object value) + { + if (TryInt(actual, convert, out var actualInt) && TryInt(expected, convert: true, value: out var expectedInt)) + { + compare = Comparer.Default.Compare(actualInt, expectedInt); + value = actualInt; + return true; + } + else if (TryLong(actual, convert, out var actualLong) && TryLong(expected, convert: true, value: out var expectedLong)) + { + compare = Comparer.Default.Compare(actualLong, expectedLong); + value = actualLong; + return true; + } + else if (TryFloat(actual, convert, out var actualFloat) && TryFloat(expected, convert: true, value: out var expectedFloat)) + { + compare = Comparer.Default.Compare(actualFloat, expectedFloat); + value = actualFloat; + return true; + } + else if (TryDateTime(actual, convert, out var actualDateTime) && TryDateTime(expected, convert: true, value: out var expectedDateTime)) + { + compare = Comparer.Default.Compare(actualDateTime, expectedDateTime); + value = actualDateTime; + return true; + } + else if ((TryStringLength(actual, out actualInt) || TryArrayLength(actual, out actualInt)) && TryInt(expected, convert: true, value: out expectedInt)) + { + compare = Comparer.Default.Compare(actualInt, expectedInt); + value = actualInt; + return true; + } + compare = 0; + value = 0; + return false; + } + + internal static bool TryString(object o, out string value) + { + o = GetBaseObject(o); + if (o is string s) + { + value = s; + return true; + } + else if (o is JToken token && token.Type == JTokenType.String) + { + value = token.Value(); + return true; + } + value = null; + return false; + } + + internal static bool TryConvertString(object o, out string value) + { + if (TryString(o, out value)) + return true; + + if (TryInt(o, false, out var ivalue)) + { + value = ivalue.ToString(Thread.CurrentThread.CurrentCulture); + return true; + } + return false; + } + + internal static bool TryConvertStringArray(object[] o, out string[] value) + { + value = Array.Empty(); + if (o == null || o.Length == 0 || !TryConvertString(o[0], out var s)) + return false; + + value = new string[o.Length]; + value[0] = s; + for (var i = 1; i < o.Length; i++) + { + if (TryConvertString(o[i], out s)) + value[i] = s; + } + return true; + } + + /// + /// Try to get an int from the existing object. + /// + internal static bool TryInt(object o, bool convert, out int value) + { + o = GetBaseObject(o); + if (o is int ivalue) + { + value = ivalue; + return true; + } + if (o is long lvalue && lvalue <= int.MaxValue && lvalue >= int.MinValue) + { + value = (int)lvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && int.TryParse(s, out ivalue)) + { + value = ivalue; + return true; + } + value = default; + return false; + } + + internal static bool TryBool(object o, bool convert, out bool value) + { + o = GetBaseObject(o); + if (o is bool bvalue) + { + value = bvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Boolean) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && bool.TryParse(s, out bvalue)) + { + value = bvalue; + return true; + } + value = default; + return false; + } + + internal static bool TryByte(object o, bool convert, out byte value) + { + o = GetBaseObject(o); + if (o is byte bvalue) + { + value = bvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && byte.TryParse(s, out bvalue)) + { + value = bvalue; + return true; + } + value = default; + return false; + } + + internal static bool TryLong(object o, bool convert, out long value) + { + o = GetBaseObject(o); + if (o is byte b) + { + value = b; + return true; + } + else if (o is int i) + { + value = i; + return true; + } + else if (o is long l) + { + value = l; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Integer) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && long.TryParse(s, out l)) + { + value = l; + return true; + } + value = default; + return false; + } + + internal static bool TryFloat(object o, bool convert, out float value) + { + o = GetBaseObject(o); + if (o is float fvalue || (convert && o is string s && float.TryParse(s, out fvalue))) + { + value = fvalue; + return true; + } + else if (convert && o is int ivalue) + { + value = ivalue; + return true; + } + value = default; + return false; + } + + internal static bool TryDouble(object o, bool convert, out double value) + { + o = GetBaseObject(o); + if (o is double dvalue || (convert && o is string s && double.TryParse(s, out dvalue))) + { + value = dvalue; + return true; + } + value = default; + return false; + } + + internal static bool TryStringLength(object o, out int value) + { + o = GetBaseObject(o); + if (o is string s) + { + value = s.Length; + return true; + } + value = 0; + return false; + } + + internal static bool TryArrayLength(object o, out int value) + { + o = GetBaseObject(o); + if (o is Array array) + { + value = array.Length; + return true; + } + value = 0; + return false; + } + + internal static bool TryDateTime(object o, bool convert, out DateTime value) + { + o = GetBaseObject(o); + if (o is DateTime dvalue) + { + value = dvalue; + return true; + } + else if (o is JToken token && token.Type == JTokenType.Date) + { + value = token.Value(); + return true; + } + else if (convert && TryString(o, out var s) && DateTime.TryParse(s, out dvalue)) + { + value = dvalue; + return true; + } + else if (convert && TryInt(o, convert: false, out var daysOffset)) + { + value = DateTime.Now.AddDays(daysOffset); + return true; + } + value = default; + return false; + } + + internal static bool Match(string pattern, string value, bool caseSensitive) + { + var expression = GetRegularExpression(pattern, caseSensitive); + return expression.IsMatch(value); + } + + internal static bool Match(object pattern, object value, bool caseSensitive) + { + return TryString(pattern, out var patternString) && TryString(value, out var s) && Match(patternString, s, caseSensitive); + } + + internal static bool StartsWith(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out var expected)) + return false; + + return actualValue.StartsWith(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + internal static bool EndsWith(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out var expected)) + return false; + + return actualValue.EndsWith(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase); + } + + internal static bool Contains(string actualValue, object expectedValue, bool caseSensitive) + { + if (!TryString(expectedValue, out var expected)) + return false; + + return actualValue.IndexOf(expected, caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) >= 0; + } + + internal static bool IsLower(string actualValue, bool requireLetters, out bool notLetter) + { + notLetter = false; + for (var i = 0; i < actualValue.Length; i++) + { + if (!char.IsLetter(actualValue, i) && requireLetters) + { + notLetter = true; + return false; + } + if (char.IsLetter(actualValue, i) && !char.IsLower(actualValue, i)) + return false; + } + return true; + } + + internal static bool IsUpper(string actualValue, bool requireLetters, out bool notLetter) + { + notLetter = false; + for (var i = 0; i < actualValue.Length; i++) + { + if (!char.IsLetter(actualValue, i) && requireLetters) + { + notLetter = true; + return false; + } + if (char.IsLetter(actualValue, i) && !char.IsUpper(actualValue, i)) + return false; + } + return true; + } + + internal static bool AnyValue(object actualValue, object expectedValue, bool caseSensitive, out object foundValue) + { + foundValue = actualValue; + var expectedBase = GetBaseObject(expectedValue); + if (actualValue is IEnumerable items) + { + foreach (var item in items) + { + foundValue = item; + if (Equal(expectedBase, item, caseSensitive)) + return true; + } + } + if (Equal(expectedBase, actualValue, caseSensitive)) + { + foundValue = actualValue; + return true; + } + return false; + } + + internal static bool WithinPath(string actualPath, string expectedPath, bool caseSensitive) + { + var expected = PSDocumentOption.GetRootedBasePath(expectedPath).Replace(Backslash, Slash); + var actual = PSDocumentOption.GetRootedPath(actualPath).Replace(Backslash, Slash); + return actual.StartsWith(expected, ignoreCase: !caseSensitive, Thread.CurrentThread.CurrentCulture); + } + + internal static string NormalizePath(string basePath, string path) + { + path = Path.IsPathRooted(path) ? Path.GetFullPath(path) : Path.GetFullPath(Path.Combine(basePath, path)); + basePath = PSDocumentOption.GetRootedBasePath(basePath); + return path.Substring(basePath.Length).Replace(Backslash, Slash); + } + + private static Regex GetRegularExpression(string pattern, bool caseSensitive) + { + if (!TryPipelineCache(caseSensitive ? CACHE_MATCH_C : CACHE_MATCH, pattern, out Regex expression)) + { + var options = caseSensitive ? RegexOptions.None : RegexOptions.IgnoreCase; + expression = new Regex(pattern, options); + SetPipelineCache(CACHE_MATCH, pattern, expression); + } + return expression; + } + + /// + /// Try to retrieve the cached key from the pipeline cache. + /// + private static bool TryPipelineCache(string prefix, string key, out T value) + { + value = default; + if (RunspaceContext.CurrentThread.ExpressionCache.TryGetValue(string.Concat(prefix, key), out var ovalue)) + { + value = (T)ovalue; + return true; + } + return false; + } + + private static void SetPipelineCache(string prefix, string key, T value) + { + RunspaceContext.CurrentThread.ExpressionCache[string.Concat(prefix, key)] = value; + } + + private static object GetBaseObject(object o) + { + return o is PSObject pso && pso.BaseObject != null ? pso.BaseObject : o; + } + + private static bool StringEqual(string expectedValue, string actualValue, bool caseSensitive) + { + return caseSensitive ? StringComparer.Ordinal.Equals(expectedValue, actualValue) : StringComparer.OrdinalIgnoreCase.Equals(expectedValue, actualValue); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/HashtableExtensions.cs b/packages/psdocs/src/PSDocs/Common/HashtableExtensions.cs new file mode 100644 index 00000000..a81f5420 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/HashtableExtensions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; + +namespace PSDocs +{ + internal static class HashtableExtensions + { + [DebuggerStepThrough] + public static void AddUnique(this Hashtable hashtable, Hashtable values) + { + if (values == null) + return; + + foreach (var key in values.Keys) + if (!hashtable.ContainsKey(key)) + hashtable.Add(key, values[key]); + } + + /// + /// Build index to allow mapping. + /// + public static Dictionary BuildIndex(this Hashtable hashtable, bool caseSensitive = false) + { + var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; + var index = new Dictionary(comparer); + foreach (DictionaryEntry entry in hashtable) + index.Add(entry.Key.ToString(), entry.Value); + + return index; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/IBindingContext.cs b/packages/psdocs/src/PSDocs/Common/IBindingContext.cs new file mode 100644 index 00000000..6067f079 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/IBindingContext.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs +{ + internal interface IBindingContext + { + bool GetNameToken(string expression, out NameToken nameToken); + + void CacheNameToken(string expression, NameToken nameToken); + } +} diff --git a/packages/psdocs/src/PSDocs/Common/JsonConverters.cs b/packages/psdocs/src/PSDocs/Common/JsonConverters.cs new file mode 100644 index 00000000..bdcc7619 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/JsonConverters.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using Newtonsoft.Json; +using PSDocs.Pipeline; +using PSDocs.Resources; + +namespace PSDocs +{ + /// + /// A custom serializer to correctly convert PSObject properties to JSON instead of CLIXML. + /// + internal sealed class PSObjectJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PSObject); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not PSObject pso) + throw new ArgumentException(message: PSDocsResources.SerializeNullPSObject, paramName: nameof(value)); + + if (WriteFileSystemInfo(writer, value, serializer) || WriteBaseObject(writer, pso, serializer)) + return; + + writer.WriteStartObject(); + foreach (var property in pso.Properties) + { + // Ignore properties that are not readable or can cause race condition + if (SkipSerialize(property)) + continue; + + writer.WritePropertyName(property.Name); + serializer.Serialize(writer, property.Value); + } + writer.WriteEndObject(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + // Create target object based on JObject + var result = existingValue as PSObject ?? new PSObject(); + + // Read tokens + ReadObject(value: result, reader: reader); + return result; + } + + private static bool SkipSerialize(PSPropertyInfo property) + { + return !property.IsGettable || + !property.IsInstance || + property.Value is PSDriveInfo || + property.Value is ProviderInfo || + property.Value is DirectoryInfo; + } + + private static void ReadObject(PSObject value, JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + + reader.Read(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; + + case JsonToken.StartObject: + var child = new PSObject(); + ReadObject(value: child, reader: reader); + value.Properties.Add(new PSNoteProperty(name: name, value: child)); + break; + + case JsonToken.StartArray: + var items = new List(); + reader.Read(); + var item = new PSObject(); + + while (reader.TokenType != JsonToken.EndArray) + { + ReadObject(value: item, reader: reader); + items.Add(item); + reader.Read(); + } + + value.Properties.Add(new PSNoteProperty(name: name, value: items.ToArray())); + break; + + case JsonToken.Comment: + break; + + default: + value.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + break; + } + reader.Read(); + } + } + + /// + /// Serialize a file system info object. + /// + private static bool WriteFileSystemInfo(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is not FileSystemInfo fileSystemInfo) + return false; + + serializer.Serialize(writer, fileSystemInfo.FullName); + return true; + } + + /// + /// Serialize the base object. + /// + private static bool WriteBaseObject(JsonWriter writer, PSObject value, JsonSerializer serializer) + { + if (value.BaseObject == null || value.HasNoteProperty()) + return false; + + serializer.Serialize(writer, value.BaseObject); + return true; + } + } + + /// + /// A custom serializer to convert PSObjects that may or maynot be in a JSON array to an a PSObject array. + /// + internal sealed class PSObjectArrayJsonConverter : JsonConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PSObject[]); + } + + public override bool CanWrite => false; + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject && reader.TokenType != JsonToken.StartArray) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + + var result = new List(); + var isArray = reader.TokenType == JsonToken.StartArray; + + if (isArray) + reader.Read(); + + while (reader.TokenType != JsonToken.None && (!isArray || (isArray && reader.TokenType != JsonToken.EndArray))) + { + var value = ReadObject(reader: reader); + result.Add(value); + + // Consume the EndObject token + reader.Read(); + } + return result.ToArray(); + } + + private static PSObject ReadObject(JsonReader reader) + { + if (reader.TokenType != JsonToken.StartObject || !reader.Read()) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + + var result = new PSObject(); + string name = null; + + // Read each token + while (reader.TokenType != JsonToken.EndObject) + { + switch (reader.TokenType) + { + case JsonToken.PropertyName: + name = reader.Value.ToString(); + break; + + case JsonToken.StartObject: + var value = ReadObject(reader: reader); + result.Properties.Add(new PSNoteProperty(name: name, value: value)); + break; + + case JsonToken.StartArray: + var items = ReadArray(reader: reader); + result.Properties.Add(new PSNoteProperty(name: name, value: items)); + break; + + case JsonToken.Comment: + break; + + default: + result.Properties.Add(new PSNoteProperty(name: name, value: reader.Value)); + break; + } + if (!reader.Read() || reader.TokenType == JsonToken.None) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + } + return result; + } + + private static PSObject[] ReadArray(JsonReader reader) + { + if (reader.TokenType != JsonToken.StartArray || !reader.Read()) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + + var result = new List(); + + // Read until the end of the array + while (reader.TokenType != JsonToken.EndArray) + { + switch (reader.TokenType) + { + case JsonToken.StartObject: + result.Add(ReadObject(reader: reader)); + break; + + case JsonToken.StartArray: + result.Add(PSObject.AsPSObject(ReadArray(reader))); + break; + + case JsonToken.Null: + result.Add(null); + break; + + case JsonToken.Comment: + break; + + default: + result.Add(PSObject.AsPSObject(reader.Value)); + break; + } + if (!reader.Read() || reader.TokenType == JsonToken.None) + throw new PipelineSerializationException(PSDocsResources.ReadJsonFailed); + } + return result.ToArray(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/KeyMapDictionary.cs b/packages/psdocs/src/PSDocs/Common/KeyMapDictionary.cs new file mode 100644 index 00000000..4107d81d --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/KeyMapDictionary.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; +using System.Linq; + +namespace PSDocs +{ + public abstract class KeyMapDictionary : DynamicObject, IDictionary + { + private readonly Dictionary _Map; + + protected KeyMapDictionary() + { + _Map = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + protected KeyMapDictionary(KeyMapDictionary map) + { + if (map == null) + throw new ArgumentNullException(nameof(map)); + + _Map = new Dictionary(map._Map, StringComparer.OrdinalIgnoreCase); + } + + protected KeyMapDictionary(IDictionary dictionary) + { + _Map = dictionary == null ? + new Dictionary(StringComparer.OrdinalIgnoreCase) : + new Dictionary(dictionary, StringComparer.OrdinalIgnoreCase); + } + + protected KeyMapDictionary(Hashtable hashtable) + : this() + { + Load(hashtable); + } + + public TValue this[string key] + { + get => _Map[key]; + set => _Map[key] = value; + } + + public ICollection Keys => _Map.Keys; + + public ICollection Values => _Map.Values; + + public int Count => _Map.Count; + + public bool IsReadOnly => false; + + public void Add(string key, TValue value) + { + _Map.Add(key, value); + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Clear() + { + _Map.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return ((IDictionary)_Map).Contains(item); + } + + public bool ContainsKey(string key) + { + return _Map.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((IDictionary)_Map).CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return _Map.GetEnumerator(); + } + + public bool Remove(string key) + { + return _Map.Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return ((IDictionary)_Map).Remove(item); + } + + public bool TryGetValue(string key, out TValue value) + { + return _Map.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + /// + /// Load options from a hashtable. + /// + protected void Load(Hashtable hashtable) + { + if (hashtable == null) + throw new ArgumentNullException(nameof(hashtable)); + + foreach (DictionaryEntry entry in hashtable) + _Map.Add(entry.Key.ToString(), (TValue)entry.Value); + } + + /// + /// Load options from environment variables. + /// + internal void Load(string prefix, EnvironmentHelper env, Func format = null) + { + if (env == null) + throw new ArgumentNullException(nameof(env)); + + foreach (var variable in env.WithPrefix(prefix)) + { + if (TryKeyPrefix(variable.Key, prefix, out var suffix)) + { + if (format != null) + suffix = format(suffix); + + _Map[suffix] = (TValue)variable.Value; + } + } + } + + /// + /// Load options from a dictionary. + /// + protected void Load(string prefix, IDictionary dictionary) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + + if (dictionary.Count == 0) + return; + + var keys = dictionary.Keys.ToArray(); + for (var i = 0; i < keys.Length; i++) + { + if (TryKeyPrefix(keys[i], prefix, out var suffix) && dictionary.TryPopValue(keys[i], out var value)) + _Map[suffix] = (TValue)value; + } + } + + /// + /// Try a key prefix. + /// + private static bool TryKeyPrefix(string key, string prefix, out string suffix) + { + suffix = key; + if (prefix == null || prefix.Length == 0) + return true; + + if (key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + suffix = key.Substring(prefix.Length); + return true; + } + return false; + } + + public sealed override bool TryGetMember(GetMemberBinder binder, out object result) + { + if (binder == null) + throw new ArgumentNullException(nameof(binder)); + + var found = _Map.TryGetValue(binder.Name, out var value); + result = value; + return found; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/NameToken.cs b/packages/psdocs/src/PSDocs/Common/NameToken.cs new file mode 100644 index 00000000..c8defdfa --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/NameToken.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace PSDocs +{ + /// + /// The type of NameToken. + /// + internal enum NameTokenType + { + /// + /// The token represents a field/ property of an object. + /// + Field = 0, + + /// + /// The token is an index in an object. + /// + Index = 1, + + /// + /// The token is a reference to the parent object. Can only be the first token. + /// + Self = 2 + } + + /// + /// A token for expressing a path through a tree of fields. + /// + [DebuggerDisplay("{Type}, Name = {Name}, Index = {Index}")] + internal sealed class NameToken + { + /// + /// The name of the field if the token type if Field. + /// + public string Name; + + /// + /// The index if the token type if Index. + /// + public int Index; + + /// + /// The next token. + /// + public NameToken Next; + + /// + /// The type of the token. + /// + public NameTokenType Type = NameTokenType.Field; + } +} diff --git a/packages/psdocs/src/PSDocs/Common/ObjectHelper.cs b/packages/psdocs/src/PSDocs/Common/ObjectHelper.cs new file mode 100644 index 00000000..743a24d6 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/ObjectHelper.cs @@ -0,0 +1,303 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Dynamic; +using System.Management.Automation; +using System.Reflection; +using System.Threading; + +namespace PSDocs +{ + internal static class ObjectHelper + { + public static object GetBaseObject(object o) + { + return o is PSObject pso && pso.BaseObject != null ? pso.BaseObject : o; + } + + private sealed class NameTokenStream + { + private const char Separator = '.'; + private const char Quoted = '\''; + private const char OpenIndex = '['; + private const char CloseIndex = ']'; + + private readonly string Name; + private readonly int Last; + + private bool inQuote; + private bool inIndex; + + public int Position = -1; + public char Current; + + public NameTokenStream(string name) + { + Name = name; + Last = Name.Length - 1; + } + + /// + /// Find the start of the sequence. + /// + /// Return true when more characters follow. + public bool Next() + { + if (Position < Last) + { + Position++; + if (Name[Position] == Separator && Position > 0) + { + Position++; + } + else if (Name[Position] == Quoted) + { + Position++; + inQuote = true; + } + Current = Name[Position]; + return true; + } + return false; + } + + /// + /// Find the end of the sequence and return the index. + /// + /// The index of the sequence end. + public int IndexOf(out NameTokenType tokenType) + { + tokenType = Position == 0 && Current == Separator ? NameTokenType.Self : NameTokenType.Field; + if (tokenType == NameTokenType.Self) + return Position; + + while (Position < Last) + { + Position++; + Current = Name[Position]; + + if (inQuote) + { + if (Current == Quoted) + { + inQuote = false; + return Position - 1; + } + } + else if (Current == Separator) + { + return Position - 1; + } + else if (inIndex) + { + if (Current == CloseIndex) + { + tokenType = NameTokenType.Index; + inIndex = false; + return Position - 1; + } + } + else if (Current == OpenIndex) + { + // Next token will be an Index + inIndex = true; + + // Return end of token + return Position - 1; + } + } + return Position; + } + + public NameToken Get() + { + var token = new NameToken(); + var result = token; + while (Next()) + { + var start = Position; + if (start > 0) + { + token.Next = new NameToken(); + token = token.Next; + } + + // Jump to the next separator or end + var end = IndexOf(out var tokenType); + token.Type = tokenType; + if (tokenType == NameTokenType.Field) + { + token.Name = Name.Substring(start, end - start + 1); + } + else if (tokenType == NameTokenType.Index) + { + token.Index = int.Parse(Name.Substring(start, end - start + 1), Thread.CurrentThread.CurrentCulture); + } + } + return result; + } + } + + private sealed class DynamicPropertyBinder : GetMemberBinder + { + internal DynamicPropertyBinder(string name, bool ignoreCase) + : base(name, ignoreCase) { } + + public override DynamicMetaObject FallbackGetMember(DynamicMetaObject target, DynamicMetaObject errorSuggestion) + { + return null; + } + } + + public static bool GetField(PSObject targetObject, string name, bool caseSensitive, out object value) + { + if (targetObject.BaseObject is IDictionary dictionary) + return TryDictionary(dictionary, name, caseSensitive, out value); + + return TryPropertyValue(targetObject, name, caseSensitive, out value); + } + + public static bool GetField(IBindingContext bindingContext, object targetObject, string name, bool caseSensitive, out object value) + { + // Try to load nameToken from cache + if (bindingContext == null || !bindingContext.GetNameToken(expression: name, nameToken: out var nameToken)) + { + nameToken = GetNameToken(expression: name); + bindingContext?.CacheNameToken(expression: name, nameToken: nameToken); + } + return GetField(targetObject: targetObject, token: nameToken, caseSensitive: caseSensitive, value: out value); + } + + private static bool GetField(object targetObject, NameToken token, bool caseSensitive, out object value) + { + value = null; + var baseObject = GetBaseObject(targetObject); + if (baseObject == null) + return false; + + var baseType = baseObject.GetType(); + object field = null; + var foundField = false; + + // Handle this object + if (token.Type == NameTokenType.Self) + { + field = baseObject; + foundField = true; + } + // Handle dictionaries and hashtables + else if (token.Type == NameTokenType.Field && baseObject is IDictionary dictionary) + { + if (TryDictionary(dictionary, token.Name, caseSensitive, out field)) + foundField = true; + } + // Handle PSObjects + else if (token.Type == NameTokenType.Field && targetObject is PSObject psObject) + { + if (TryPropertyValue(psObject, token.Name, caseSensitive, out field)) + foundField = true; + } + // Handle DynamicObjects + else if (token.Type == NameTokenType.Field && targetObject is DynamicObject dynamicObject) + { + if (TryPropertyValue(dynamicObject, token.Name, caseSensitive, out field)) + foundField = true; + } + // Handle all other CLR types + else if (token.Type == NameTokenType.Field) + { + if (TryPropertyValue(targetObject, token.Name, baseType, caseSensitive, out field) || TryFieldValue(targetObject, token.Name, baseType, caseSensitive, out field)) + foundField = true; + } + // Handle Index tokens + else if (baseType.IsArray && baseObject is Array array && token.Index < array.Length) + { + field = array.GetValue(token.Index); + foundField = true; + } + + if (foundField) + { + if (token.Next == null) + { + value = field; + return true; + } + else + { + return GetField(targetObject: field, token: token.Next, caseSensitive: caseSensitive, value: out value); + } + } + return false; + } + + private static bool TryDictionary(IDictionary dictionary, string key, bool caseSensitive, out object value) + { + value = null; + var comparer = caseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase; + foreach (var k in dictionary.Keys) + { + if (comparer.Equals(key, k)) + { + value = dictionary[k]; + return true; + } + } + return false; + } + + private static bool TryPropertyValue(object targetObject, string propertyName, Type baseType, bool caseSensitive, out object value) + { + value = null; + var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase; + var propertyInfo = baseType.GetProperty(propertyName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public); + if (propertyInfo == null) + return false; + + value = propertyInfo.GetValue(targetObject); + return true; + } + + private static bool TryPropertyValue(PSObject targetObject, string propertyName, bool caseSensitive, out object value) + { + value = null; + var p = targetObject.Properties[propertyName]; + if (p == null) + return false; + + if (caseSensitive && !StringComparer.Ordinal.Equals(p.Name, propertyName)) + return false; + + value = p.Value; + return true; + } + + private static bool TryPropertyValue(DynamicObject targetObject, string propertyName, bool caseSensitive, out object value) + { + if (!targetObject.TryGetMember(new DynamicPropertyBinder(propertyName, !caseSensitive), out value)) + return false; + + return true; + } + + private static bool TryFieldValue(object targetObject, string fieldName, Type baseType, bool caseSensitive, out object value) + { + value = null; + var bindingFlags = caseSensitive ? BindingFlags.Default : BindingFlags.IgnoreCase; + var fieldInfo = baseType.GetField(fieldName, bindingAttr: bindingFlags | BindingFlags.Instance | BindingFlags.Public); + if (fieldInfo == null) + return false; + + value = fieldInfo.GetValue(targetObject); + return true; + } + + private static NameToken GetNameToken(string expression) + { + var stream = new NameTokenStream(expression); + return stream.Get(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/PSObjectExtensions.cs b/packages/psdocs/src/PSDocs/Common/PSObjectExtensions.cs new file mode 100644 index 00000000..45fc994a --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/PSObjectExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs +{ + internal static class PSObjectExtensions + { + public static bool HasProperty(this PSObject o, string propertyName) + { + return o.Properties[propertyName] != null; + } + + /// + /// Determines if the PSObject has any note properties. + /// + public static bool HasNoteProperty(this PSObject o) + { + foreach (var property in o.Properties) + { + if (property.MemberType == PSMemberTypes.NoteProperty) + return true; + } + return false; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/StringExtensions.cs b/packages/psdocs/src/PSDocs/Common/StringExtensions.cs new file mode 100644 index 00000000..7b1b3ce1 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/StringExtensions.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace PSDocs +{ + internal static class StringExtensions + { + public static bool IsUri(this string s) + { + return s.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || s.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Common/YamlConverters.cs b/packages/psdocs/src/PSDocs/Common/YamlConverters.cs new file mode 100644 index 00000000..75eca36b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Common/YamlConverters.cs @@ -0,0 +1,552 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PSDocs.Annotations; +using PSDocs.Configuration; +using PSDocs.Definitions; +using PSDocs.Definitions.Selectors; +using PSDocs.Runtime; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.TypeInspectors; +using YamlDotNet.Serialization.TypeResolvers; + +namespace PSDocs +{ + /// + /// A YAML converter for deserializing a string array. + /// + internal sealed class StringArrayTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type == typeof(string[]) || type == typeof(IEnumerable); + } + + public object ReadYaml(IParser parser, Type type) + { + var result = new List(); + var isSequence = parser.TryConsume(out _); + while ((isSequence || result.Count == 0) && parser.TryConsume(out Scalar scalar)) + result.Add(scalar.Value); + + if (isSequence) + { + parser.Require(); + parser.MoveNext(); + } + return result.ToArray(); + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + throw new NotImplementedException(); + } + } + + internal sealed class PSObjectTypeInspector : TypeInspectorSkeleton + { + private readonly ITypeInspector _Parent; + private readonly ITypeResolver _TypeResolver; + private readonly INamingConvention _NamingConvention; + + public PSObjectTypeInspector(ITypeInspector typeInspector) + { + _Parent = typeInspector; + _TypeResolver = new StaticTypeResolver(); + _NamingConvention = CamelCaseNamingConvention.Instance; + } + + public override IEnumerable GetProperties(Type type, object container) + { + if (container is PSObject pso) + return GetPropertyDescriptor(pso); + + return _Parent.GetProperties(type, container); + } + + private IEnumerable GetPropertyDescriptor(PSObject pso) + { + foreach (var prop in pso.Properties) + { + if (prop.IsGettable && prop.IsInstance) + yield return new Property(prop, _TypeResolver, _NamingConvention); + } + } + + private sealed class Property : IPropertyDescriptor + { + private readonly PSPropertyInfo _PropertyInfo; + private readonly Type _PropertyType; + private readonly ITypeResolver _TypeResolver; + private readonly INamingConvention _NamingConvention; + + public Property(PSPropertyInfo propertyInfo, ITypeResolver typeResolver, INamingConvention namingConvention) + { + _PropertyInfo = propertyInfo; + _PropertyType = propertyInfo.Value.GetType(); + _TypeResolver = typeResolver; + _NamingConvention = namingConvention; + ScalarStyle = ScalarStyle.Any; + } + + string IPropertyDescriptor.Name => _NamingConvention.Apply(_PropertyInfo.Name); + + public Type Type => _PropertyType; + + public Type TypeOverride { get; set; } + + int IPropertyDescriptor.Order { get; set; } + + bool IPropertyDescriptor.CanWrite => false; + + public ScalarStyle ScalarStyle { get; set; } + + public T GetCustomAttribute() where T : Attribute + { + return default; + } + + void IPropertyDescriptor.Write(object target, object value) + { + throw new NotImplementedException(); + } + + IObjectDescriptor IPropertyDescriptor.Read(object target) + { + var propertyValue = _PropertyInfo.Value; + var actualType = TypeOverride ?? _TypeResolver.Resolve(Type, propertyValue); + return new ObjectDescriptor(propertyValue, actualType, Type, ScalarStyle); + } + } + } + + /// + /// A YAML converter to deserialize a map/ object as a PSObject. + /// + internal sealed class PSObjectYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type == typeof(PSObject); + } + + public object ReadYaml(IParser parser, Type type) + { + // Handle empty objects + if (parser.TryConsume(out _)) + { + parser.TryConsume(out _); + return null; + } + + var result = new PSObject(); + if (parser.TryConsume(out _)) + { + while (parser.TryConsume(out Scalar scalar)) + { + var name = scalar.Value; + var property = ReadNoteProperty(parser, name); + if (property == null) + throw new NotImplementedException(); + + result.Properties.Add(property); + } + parser.Require(); + parser.MoveNext(); + } + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + throw new NotImplementedException(); + } + + private PSNoteProperty ReadNoteProperty(IParser parser, string name) + { + if (parser.TryConsume(out _)) + { + var values = new List(); + while (parser.Current is not SequenceEnd) + { + if (parser.Current is MappingStart) + { + values.Add(PSObject.AsPSObject(ReadYaml(parser, typeof(PSObject)))); + } + else if (parser.TryConsume(out Scalar scalar)) + { + values.Add(PSObject.AsPSObject(scalar.Value)); + } + } + parser.Require(); + parser.MoveNext(); + return new PSNoteProperty(name, values.ToArray()); + } + else if (parser.Current is MappingStart) + { + return new PSNoteProperty(name, ReadYaml(parser, typeof(PSObject))); + } + else if (parser.TryConsume(out Scalar scalar)) + { + return new PSNoteProperty(name, scalar.Value); + } + return null; + } + } + + /// + /// A YAML resolver to convert any dictionary types to PSObjects instead. + /// + internal sealed class PSObjectYamlTypeResolver : INodeTypeResolver + { + public bool Resolve(NodeEvent nodeEvent, ref Type currentType) + { + if (currentType == typeof(Dictionary) || nodeEvent is MappingStart) + { + currentType = typeof(PSObject); + return true; + } + return false; + } + } + + /// + /// A YAML converter for de/serializing a field map. + /// + internal sealed class FieldMapYamlTypeConverter : IYamlTypeConverter + { + public bool Accepts(Type type) + { + return type == typeof(FieldMap); + } + + public object ReadYaml(IParser parser, Type type) + { + var result = new FieldMap(); + if (parser.TryConsume(out _)) + { + while (parser.TryConsume(out Scalar scalar)) + { + var fieldName = scalar.Value; + if (parser.TryConsume(out _)) + { + var fields = new List(); + while (!parser.Accept(out _)) + { + if (parser.TryConsume(out scalar)) + fields.Add(scalar.Value); + } + result.Set(fieldName, fields.ToArray()); + parser.Require(); + parser.MoveNext(); + } + } + parser.Require(); + parser.MoveNext(); + } + return result; + } + + public void WriteYaml(IEmitter emitter, object value, Type type) + { + if (type == typeof(FieldMap) && value == null) + { + emitter.Emit(new MappingStart()); + emitter.Emit(new MappingEnd()); + } + if (value is not FieldMap map) + return; + + emitter.Emit(new MappingStart()); + foreach (var field in map) + { + emitter.Emit(new Scalar(field.Key)); + emitter.Emit(new SequenceStart(null, null, false, SequenceStyle.Block)); + for (var i = 0; i < field.Value.Length; i++) + { + emitter.Emit(new Scalar(field.Value[i])); + } + emitter.Emit(new SequenceEnd()); + } + emitter.Emit(new MappingEnd()); + } + } + + internal sealed class LanguageBlockDeserializer : INodeDeserializer + { + private const string FIELD_APIVERSION = "apiVersion"; + private const string FIELD_KIND = "kind"; + private const string FIELD_METADATA = "metadata"; + private const string FIELD_SPEC = "spec"; + + private readonly INodeDeserializer _Next; + private readonly SpecFactory _Factory; + + public LanguageBlockDeserializer(INodeDeserializer next) + { + _Next = next; + _Factory = new SpecFactory(); + } + + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object value) + { + if (typeof(ResourceObject).IsAssignableFrom(expectedType)) + { + var comment = HostHelper.GetCommentMeta(RunspaceContext.CurrentThread.Source.File.Path, reader.Current.Start.Line - 2, reader.Current.Start.Column); + var resource = MapResource(reader, nestedObjectDeserializer, comment); + value = new ResourceObject(resource); + return true; + } + else + { + return _Next.Deserialize(reader, expectedType, nestedObjectDeserializer, out value); + } + } + + private IResource MapResource(IParser reader, Func nestedObjectDeserializer, CommentMetadata comment) + { + IResource result = null; + string apiVersion = null; + string kind = null; + ResourceMetadata metadata = null; + if (reader.TryConsume(out _)) + { + while (reader.TryConsume(out Scalar scalar)) + { + // Read apiVersion + if (TryApiVersion(reader, scalar, out var apiVersionValue)) + { + apiVersion = apiVersionValue; + } + // Read kind + else if (TryKind(reader, scalar, out var kindValue)) + { + kind = kindValue; + } + // Read metadata + else if (TryMetadata(reader, scalar, nestedObjectDeserializer, out var metadataValue)) + { + metadata = metadataValue; + } + // Read spec + else if (apiVersion != null && kind != null && TrySpec(reader, scalar, apiVersion, kind, nestedObjectDeserializer, metadata, comment, out var resource)) + { + result = resource; + } + else + { + reader.SkipThisAndNestedEvents(); + } + } + reader.Require(); + reader.MoveNext(); + } + return result; + } + + private static bool TryApiVersion(IParser reader, Scalar scalar, out string kind) + { + kind = null; + if (scalar.Value == FIELD_APIVERSION) + { + kind = reader.Consume().Value; + return true; + } + return false; + } + + private static bool TryKind(IParser reader, Scalar scalar, out string kind) + { + kind = null; + if (scalar.Value == FIELD_KIND) + { + kind = reader.Consume().Value; + return true; + } + return false; + } + + private bool TryMetadata(IParser reader, Scalar scalar, Func nestedObjectDeserializer, out ResourceMetadata metadata) + { + metadata = null; + if (scalar.Value != FIELD_METADATA) + return false; + + if (reader.Current is MappingStart) + { + if (!_Next.Deserialize(reader, typeof(ResourceMetadata), nestedObjectDeserializer, out var value)) + return false; + + metadata = (ResourceMetadata)value; + return true; + } + return false; + } + + private bool TrySpec(IParser reader, Scalar scalar, string apiVersion, string kind, Func nestedObjectDeserializer, ResourceMetadata metadata, CommentMetadata comment, out IResource spec) + { + spec = null; + if (scalar.Value != FIELD_SPEC) + return false; + + return TryResource(reader, apiVersion, kind, nestedObjectDeserializer, metadata, comment, out spec); + } + + private bool TryResource(IParser reader, string apiVersion, string kind, Func nestedObjectDeserializer, ResourceMetadata metadata, CommentMetadata comment, out IResource spec) + { + spec = null; + if (_Factory.TryDescriptor(apiVersion, kind, out var descriptor) && reader.Current is MappingStart) + { + if (!_Next.Deserialize(reader, descriptor.SpecType, nestedObjectDeserializer, out var value)) + return false; + + spec = descriptor.CreateInstance(RunspaceContext.CurrentThread.Source.File, metadata, comment, value); + return true; + } + return false; + } + } + + internal sealed class SelectorExpressionDeserializer : INodeDeserializer + { + private const string OPERATOR_IF = "if"; + + private readonly INodeDeserializer _Next; + private readonly SelectorExpressionFactory _Factory; + + public SelectorExpressionDeserializer(INodeDeserializer next) + { + _Next = next; + _Factory = new SelectorExpressionFactory(); + } + + bool INodeDeserializer.Deserialize(IParser reader, Type expectedType, Func nestedObjectDeserializer, out object value) + { + if (typeof(SelectorExpression).IsAssignableFrom(expectedType)) + { + var resource = MapOperator(OPERATOR_IF, reader, nestedObjectDeserializer); + value = new SelectorIf(resource); + return true; + } + else + { + return _Next.Deserialize(reader, expectedType, nestedObjectDeserializer, out value); + } + } + + private SelectorExpression MapOperator(string type, IParser reader, Func nestedObjectDeserializer) + { + if (TryExpression(reader, type, nestedObjectDeserializer, out SelectorOperator result)) + { + // If and Not + if (reader.TryConsume(out _)) + { + result.Add(MapExpression(reader, nestedObjectDeserializer)); + reader.Require(); + reader.MoveNext(); + } + // AllOf and AnyOf + else if (reader.TryConsume(out _)) + { + while (reader.TryConsume(out _)) + { + result.Add(MapExpression(reader, nestedObjectDeserializer)); + reader.Require(); + reader.MoveNext(); + } + reader.Require(); + reader.MoveNext(); + } + } + return result; + } + + private SelectorExpression MapCondition(string type, SelectorExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer) + { + if (TryExpression(reader, type, nestedObjectDeserializer, out SelectorCondition result)) + { + while (!reader.Accept(out MappingEnd end)) + { + MapProperty(properties, reader, nestedObjectDeserializer, out _); + } + result.Add(properties); + } + return result; + } + + private SelectorExpression MapExpression(IParser reader, Func nestedObjectDeserializer) + { + SelectorExpression result = null; + var properties = new SelectorExpression.PropertyBag(); + MapProperty(properties, reader, nestedObjectDeserializer, out var key); + if (key != null && TryCondition(key)) + { + result = MapCondition(key, properties, reader, nestedObjectDeserializer); + } + else if (TryOperator(key) && reader.Accept(out MappingStart mapping)) + { + result = MapOperator(key, reader, nestedObjectDeserializer); + } + else if (TryOperator(key) && reader.Accept(out SequenceStart sequence)) + { + result = MapOperator(key, reader, nestedObjectDeserializer); + } + return result; + } + + private void MapProperty(SelectorExpression.PropertyBag properties, IParser reader, Func nestedObjectDeserializer, out string name) + { + name = null; + while (reader.TryConsume(out Scalar scalar)) + { + var key = scalar.Value; + if (TryCondition(key) || TryOperator(key)) + name = key; + + if (reader.TryConsume(out scalar)) + { + properties[key] = scalar.Value; + } + else if (TryCondition(key) && reader.TryConsume(out _)) + { + var objects = new List(); + while (!reader.TryConsume(out _)) + { + if (reader.TryConsume(out scalar)) + { + objects.Add(scalar.Value); + } + } + properties[key] = objects.ToArray(); + } + } + } + + private bool TryOperator(string key) + { + return _Factory.IsOperator(key); + } + + private bool TryCondition(string key) + { + return _Factory.IsCondition(key); + } + + private bool TryExpression(IParser reader, string type, Func nestedObjectDeserializer, out T expression) where T : SelectorExpression + { + expression = null; + if (_Factory.TryDescriptor(type, out var descriptor)) + { + expression = (T)descriptor.CreateInstance(RunspaceContext.CurrentThread.Source.File, null); + return expression != null; + } + return false; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/ColumnPadding.cs b/packages/psdocs/src/PSDocs/Configuration/ColumnPadding.cs new file mode 100644 index 00000000..b774f622 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/ColumnPadding.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Configuration +{ + public enum ColumnPadding + { + None, + + Single, + + MatchHeader, + + Undefined + } +} \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/Configuration/ConfigurationOption.cs b/packages/psdocs/src/PSDocs/Configuration/ConfigurationOption.cs new file mode 100644 index 00000000..d75882af --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/ConfigurationOption.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; + +namespace PSDocs.Configuration +{ + /// + /// A set of key/ value configuration options for document definitions. + /// + public sealed class ConfigurationOption : KeyMapDictionary + { + private const string ENVIRONMENT_PREFIX = "PSDOCS_CONFIGURATION_"; + private const string DICTIONARY_PREFIX = "Configuration."; + + public ConfigurationOption() + : base() { } + + public ConfigurationOption(ConfigurationOption option) + : base(option) { } + + private ConfigurationOption(Hashtable hashtable) + : base(hashtable) { } + + public static implicit operator ConfigurationOption(Hashtable hashtable) + { + return new ConfigurationOption(hashtable); + } + + internal static ConfigurationOption Combine(ConfigurationOption o1, ConfigurationOption o2) + { + var result = new ConfigurationOption(o1); + result.AddUnique(o2); + return result; + } + + internal void Load(EnvironmentHelper env) + { + base.Load(ENVIRONMENT_PREFIX, env); + } + + internal void Load(IDictionary dictionary) + { + base.Load(DICTIONARY_PREFIX, dictionary); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/DocumentOption.cs b/packages/psdocs/src/PSDocs/Configuration/DocumentOption.cs new file mode 100644 index 00000000..ace2dfc3 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/DocumentOption.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace PSDocs.Configuration +{ + public sealed class DocumentOption : IEquatable + { + internal static readonly DocumentOption Default = new() + { + Include = null, + Tag = null, + }; + + public DocumentOption() + { + Include = null; + Tag = null; + } + + internal DocumentOption(DocumentOption option) + { + Include = option.Include; + Tag = option.Tag; + } + + public override bool Equals(object obj) + { + return obj is DocumentOption option && Equals(option); + } + + public bool Equals(DocumentOption other) + { + return other != null && + Include == other.Include && + Tag == other.Tag; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Include != null ? Include.GetHashCode() : 0); + hash = hash * 23 + (Tag != null ? Tag.GetHashCode() : 0); + return hash; + } + } + + internal static DocumentOption Combine(DocumentOption o1, DocumentOption o2) + { + return new DocumentOption(o1) + { + Include = o1.Include ?? o2.Include, + Tag = o1.Tag ?? o2.Tag + }; + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Exposed for serialization.")] + public string[] Include { get; set; } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Exposed for serialization.")] + public string[] Tag { get; set; } + + internal void Load(EnvironmentHelper env) + { + if (env.TryStringArray("PSDOCS_DOCUMENT_INCLUDE", out var include)) + Include = include; + + if (env.TryStringArray("PSDOCS_DOCUMENT_TAG", out var tag)) + Tag = tag; + } + + internal void Load(Dictionary index) + { + if (index.TryPopStringArray("Document.Include", out var include)) + Include = include; + + if (index.TryPopStringArray("Document.Tag", out var tag)) + Tag = tag; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/EdgePipeOption.cs b/packages/psdocs/src/PSDocs/Configuration/EdgePipeOption.cs new file mode 100644 index 00000000..7f999223 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/EdgePipeOption.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Configuration +{ + public enum EdgePipeOption + { + WhenRequired, + + Always + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/ExecutionOption.cs b/packages/psdocs/src/PSDocs/Configuration/ExecutionOption.cs new file mode 100644 index 00000000..e5197c09 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/ExecutionOption.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace PSDocs.Configuration +{ + /// + /// Options that affect document execution. + /// + public sealed class ExecutionOption : IEquatable + { + private const LanguageMode DEFAULT_LANGUAGEMODE = Configuration.LanguageMode.FullLanguage; + + internal static readonly ExecutionOption Default = new() + { + LanguageMode = DEFAULT_LANGUAGEMODE, + }; + + public ExecutionOption() + { + LanguageMode = null; + } + + internal ExecutionOption(ExecutionOption option) + { + LanguageMode = option.LanguageMode; + } + + public override bool Equals(object obj) + { + return obj is ExecutionOption option && Equals(option); + } + + public bool Equals(ExecutionOption other) + { + return other != null && + LanguageMode == other.LanguageMode; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (LanguageMode.HasValue ? LanguageMode.Value.GetHashCode() : 0); + return hash; + } + } + + internal static ExecutionOption Combine(ExecutionOption o1, ExecutionOption o2) + { + return new ExecutionOption(o1) + { + LanguageMode = o1.LanguageMode ?? o2.LanguageMode + }; + } + + /// + /// The PowerShell language mode to use for document execution. + /// + /// + /// The default is FullLanguage. + /// + [DefaultValue(null)] + public LanguageMode? LanguageMode { get; set; } + + internal void Load(EnvironmentHelper env) + { + if (env.TryEnum("PSDOCS_EXECUTION_LANGUAGEMODE", out LanguageMode languageMode)) + LanguageMode = languageMode; + } + + internal void Load(Dictionary index) + { + if (index.TryPopEnum("Execution.LanguageMode", out LanguageMode languageMode)) + LanguageMode = languageMode; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/FieldMap.cs b/packages/psdocs/src/PSDocs/Configuration/FieldMap.cs new file mode 100644 index 00000000..465645e5 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/FieldMap.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; + +namespace PSDocs.Configuration +{ + /// + /// A mapping of fields to property names. + /// + public sealed class FieldMap : DynamicObject, IEnumerable> + { + private readonly Dictionary _Map; + + public FieldMap() + { + _Map = new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + internal FieldMap(FieldMap map) + { + _Map = new Dictionary(map._Map, StringComparer.OrdinalIgnoreCase); + } + + internal FieldMap(Dictionary map) + { + _Map = new Dictionary(map, StringComparer.OrdinalIgnoreCase); + } + + public int Count => _Map.Count; + + public bool TryField(string fieldName, out string[] fields) + { + return _Map.TryGetValue(fieldName, out fields); + } + + internal void Set(string fieldName, string[] fields) + { + _Map[fieldName] = fields; + } + + internal static void Load(FieldMap map, Dictionary properties) + { + foreach (var property in properties) + { + if (property.Value is string value && !string.IsNullOrEmpty(value)) + map.Set(property.Key, new string[] { value }); + else if (property.Value is string[] array && array.Length > 0) + map.Set(property.Key, array); + } + } + + public IEnumerator> GetEnumerator() + { + return ((IEnumerable>)_Map).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var found = TryField(binder.Name, out var value); + result = value; + return found; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/InputFormat.cs b/packages/psdocs/src/PSDocs/Configuration/InputFormat.cs new file mode 100644 index 00000000..7cc467dc --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/InputFormat.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; + +namespace PSDocs.Configuration +{ + /// + /// The formats to convert input from. + /// + [JsonConverter(typeof(StringEnumConverter))] + public enum InputFormat + { + None = 0, + + Yaml = 1, + + Json = 2, + + PowerShellData = 3, + + Detect = 255 + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/InputOption.cs b/packages/psdocs/src/PSDocs/Configuration/InputOption.cs new file mode 100644 index 00000000..05b3ebed --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/InputOption.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace PSDocs.Configuration +{ + /// + /// Options that affect how input types are processed. + /// + public sealed class InputOption : IEquatable + { + private const InputFormat DEFAULT_FORMAT = PSDocs.Configuration.InputFormat.Detect; + private const string DEFAULT_OBJECTPATH = null; + private const string[] DEFAULT_PATHIGNORE = null; + + internal static readonly InputOption Default = new() + { + Format = DEFAULT_FORMAT, + ObjectPath = DEFAULT_OBJECTPATH, + PathIgnore = DEFAULT_PATHIGNORE, + }; + + public InputOption() + { + Format = null; + ObjectPath = null; + PathIgnore = null; + } + + public InputOption(InputOption option) + { + if (option == null) + return; + + Format = option.Format; + ObjectPath = option.ObjectPath; + PathIgnore = option.PathIgnore; + } + + public override bool Equals(object obj) + { + return obj is InputOption option && Equals(option); + } + + public bool Equals(InputOption other) + { + return other != null && + Format == other.Format && + ObjectPath == other.ObjectPath && + PathIgnore == other.PathIgnore; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Format.HasValue ? Format.Value.GetHashCode() : 0); + hash = hash * 23 + (ObjectPath != null ? ObjectPath.GetHashCode() : 0); + hash = hash * 23 + (PathIgnore != null ? PathIgnore.GetHashCode() : 0); + return hash; + } + } + + internal static InputOption Combine(InputOption o1, InputOption o2) + { + var result = new InputOption(o1) + { + Format = o1.Format ?? o2.Format, + ObjectPath = o1.ObjectPath ?? o2.ObjectPath, + PathIgnore = o1.PathIgnore ?? o2.PathIgnore + }; + return result; + } + + /// + /// The input string format. + /// + [DefaultValue(null)] + public InputFormat? Format { get; set; } + + /// + /// The object path to a property to use instead of the pipeline object. + /// + [DefaultValue(null)] + public string ObjectPath { get; set; } + + /// + /// Ignores input files that match the path spec. + /// + [DefaultValue(null)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Exposed for serialization.")] + public string[] PathIgnore { get; set; } + + internal void Load(EnvironmentHelper env) + { + if (env.TryEnum("PSDOCS_INPUT_FORMAT", out InputFormat format)) + Format = format; + + if (env.TryString("PSDOCS_INPUT_OBJECTPATH", out var objectPath)) + ObjectPath = objectPath; + + if (env.TryStringArray("PSDOCS_INPUT_PATHIGNORE", out var pathIgnore)) + PathIgnore = pathIgnore; + } + + internal void Load(Dictionary index) + { + if (index.TryPopEnum("Input.Format", out InputFormat format)) + Format = format; + + if (index.TryPopString("Input.ObjectPath", out var objectPath)) + ObjectPath = objectPath; + + if (index.TryPopStringArray("Input.PathIgnore", out var pathIgnore)) + PathIgnore = pathIgnore; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/LanguageMode.cs b/packages/psdocs/src/PSDocs/Configuration/LanguageMode.cs new file mode 100644 index 00000000..ea81c0ed --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/LanguageMode.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Configuration +{ + public enum LanguageMode + { + FullLanguage = 0, + + ConstrainedLanguage = 1 + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/MarkdownEncoding.cs b/packages/psdocs/src/PSDocs/Configuration/MarkdownEncoding.cs new file mode 100644 index 00000000..e3b11d72 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/MarkdownEncoding.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Configuration +{ + public enum MarkdownEncoding + { + Default = 0, + + UTF8, + + UTF7, + + Unicode, + + UTF32, + + ASCII + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/MarkdownOption.cs b/packages/psdocs/src/PSDocs/Configuration/MarkdownOption.cs new file mode 100644 index 00000000..154ece6c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/MarkdownOption.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace PSDocs.Configuration +{ + /// + /// Options that affect markdown formatting. + /// + public sealed class MarkdownOption : IEquatable + { + private const string DEFAULT_WRAP_SEPARATOR = " "; + private const MarkdownEncoding DEFAULT_ENCODING = MarkdownEncoding.Default; + private const bool DEFAULT_SKIP_EMPTY_SECTIONS = true; + private const ColumnPadding DEFAULT_COLUMN_PADDING = Configuration.ColumnPadding.MatchHeader; + private const EdgePipeOption DEFAULT_USE_EDGE_PIPES = EdgePipeOption.WhenRequired; + + internal static readonly MarkdownOption Default = new() + { + ColumnPadding = DEFAULT_COLUMN_PADDING, + Encoding = DEFAULT_ENCODING, + SkipEmptySections = DEFAULT_SKIP_EMPTY_SECTIONS, + UseEdgePipes = DEFAULT_USE_EDGE_PIPES, + WrapSeparator = DEFAULT_WRAP_SEPARATOR, + }; + + public MarkdownOption() + { + ColumnPadding = null; + Encoding = null; + SkipEmptySections = null; + UseEdgePipes = null; + WrapSeparator = null; + } + + internal MarkdownOption(MarkdownOption option) + { + ColumnPadding = option.ColumnPadding; + Encoding = option.Encoding; + SkipEmptySections = option.SkipEmptySections; + UseEdgePipes = option.UseEdgePipes; + WrapSeparator = option.WrapSeparator; + } + + public override bool Equals(object obj) + { + return obj is MarkdownOption option && Equals(option); + } + + public bool Equals(MarkdownOption other) + { + return other != null && + ColumnPadding == other.ColumnPadding && + Encoding == other.Encoding && + SkipEmptySections == other.SkipEmptySections && + UseEdgePipes == other.UseEdgePipes && + WrapSeparator == other.WrapSeparator; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (ColumnPadding.HasValue ? ColumnPadding.Value.GetHashCode() : 0); + hash = hash * 23 + (Encoding.HasValue ? Encoding.Value.GetHashCode() : 0); + hash = hash * 23 + (SkipEmptySections.HasValue ? SkipEmptySections.GetHashCode() : 0); + hash = hash * 23 + (UseEdgePipes.HasValue ? UseEdgePipes.Value.GetHashCode() : 0); + hash = hash * 23 + (WrapSeparator != null ? WrapSeparator.GetHashCode() : 0); + return hash; + } + } + + internal static MarkdownOption Combine(MarkdownOption o1, MarkdownOption o2) + { + return new MarkdownOption(o1) + { + ColumnPadding = o1.ColumnPadding ?? o2.ColumnPadding, + Encoding = o1.Encoding ?? o2.Encoding, + SkipEmptySections = o1.SkipEmptySections ?? o2.SkipEmptySections, + UseEdgePipes = o1.UseEdgePipes ?? o2.UseEdgePipes, + WrapSeparator = o1.WrapSeparator ?? o2.WrapSeparator + }; + } + + /// + /// Determines how table columns are padded. + /// + /// + /// Defaults to MatchHeader. + /// + [DefaultValue(null)] + public ColumnPadding? ColumnPadding { get; set; } + + /// + /// + /// + /// + /// Defaults to UTF-8 without byte order mark (BOM) + /// + [DefaultValue(null)] + public MarkdownEncoding? Encoding { get; set; } + + /// + /// Determines if empty sections are included in output. + /// + /// + /// Defaults to true. + /// + [DefaultValue(null)] + public bool? SkipEmptySections { get; set; } + + /// + /// Determines when pipes on the edge of a table should be used. + /// + /// + /// Defaults to WhenRequired. + /// + [DefaultValue(null)] + public EdgePipeOption? UseEdgePipes { get; set; } + + /// + /// Specifies the character/ string to use when wrapping lines in a table cell. + /// + /// + /// Defaults to ' '. + /// + [DefaultValue(null)] + public string WrapSeparator { get; set; } + + internal void Load(EnvironmentHelper env) + { + if (env.TryEnum("PSDOCS_MARKDOWN_COLUMNPADDING", out ColumnPadding columnPadding)) + ColumnPadding = columnPadding; + + if (env.TryEnum("PSDOCS_MARKDOWN_ENCODING", out MarkdownEncoding encoding)) + Encoding = encoding; + + if (env.TryBool("PSDOCS_MARKDOWN_SKIPEMPTYSECTIONS", out var skipEmptySections)) + SkipEmptySections = skipEmptySections; + + if (env.TryEnum("PSDOCS_MARKDOWN_USEEDGEPIPES", out EdgePipeOption useEdgePipes)) + UseEdgePipes = useEdgePipes; + + if (env.TryString("PSDOCS_MARKDOWN_WRAPSEPARATOR", out var wrapSeparator)) + WrapSeparator = wrapSeparator; + } + + internal void Load(Dictionary index) + { + if (index.TryPopEnum("Markdown.ColumnPadding", out ColumnPadding columnPadding)) + ColumnPadding = columnPadding; + + if (index.TryPopEnum("Markdown.Encoding", out MarkdownEncoding encoding)) + Encoding = encoding; + + if (index.TryPopBool("Markdown.SkipEmptySections", out var skipEmptySections)) + SkipEmptySections = skipEmptySections; + + if (index.TryPopEnum("Markdown.UseEdgePipes", out EdgePipeOption useEdgePipes)) + UseEdgePipes = useEdgePipes; + + if (index.TryPopString("Markdown.WrapSeparator", out var wrapSeparator)) + WrapSeparator = wrapSeparator; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/OutputOption.cs b/packages/psdocs/src/PSDocs/Configuration/OutputOption.cs new file mode 100644 index 00000000..64ac5ed2 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/OutputOption.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.ComponentModel; + +namespace PSDocs.Configuration +{ + /// + /// Options that affect how output is generated. + /// + public sealed class OutputOption : IEquatable + { + internal static readonly OutputOption Default = new() + { + Culture = null, + Path = null, + }; + + public OutputOption() + { + Culture = null; + Path = null; + } + + internal OutputOption(OutputOption option) + { + Culture = option.Culture; + Path = option.Path; + } + + public override bool Equals(object obj) + { + return obj is OutputOption option && Equals(option); + } + + public bool Equals(OutputOption other) + { + return other != null && + Culture == other.Culture && + Path == other.Path; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Culture != null ? Culture.GetHashCode() : 0); + hash = hash * 23 + (Path != null ? Path.GetHashCode() : 0); + return hash; + } + } + + internal static OutputOption Combine(OutputOption o1, OutputOption o2) + { + return new OutputOption(o1) + { + Culture = o1.Culture ?? o2.Culture, + Path = o1.Path ?? o2.Path + }; + } + + [DefaultValue(null)] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1819:Properties should not return arrays", Justification = "Exposed for serialization.")] + public string[] Culture { get; set; } + + /// + /// The file path location to save results. + /// + [DefaultValue(null)] + public string Path { get; set; } + + internal void Load(EnvironmentHelper env) + { + if (env.TryStringArray("PSDOCS_OUTPUT_CULTURE", out var culture)) + Culture = culture; + + if (env.TryString("PSDOCS_OUTPUT_PATH", out var path)) + Path = path; + } + + internal void Load(Dictionary index) + { + if (index.TryPopStringArray("Output.Culture", out var culture)) + Culture = culture; + + if (index.TryPopString("Output.Path", out var path)) + Path = path; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Configuration/PSDocumentOption.cs b/packages/psdocs/src/PSDocs/Configuration/PSDocumentOption.cs new file mode 100644 index 00000000..14084b24 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Configuration/PSDocumentOption.cs @@ -0,0 +1,455 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Threading; +using PSDocs.Resources; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; + +namespace PSDocs.Configuration +{ + /// + /// A delgate to allow callback to PowerShell to get current working path. + /// + internal delegate string PathDelegate(); + + public interface IPSDocumentOption + { + /// + /// A set of key/ value configuration options for document definitions. + /// + ConfigurationOption Configuration { get; } + + DocumentOption Document { get; } + + /// + /// Options that affect document execution. + /// + ExecutionOption Execution { get; } + + /// + /// Options that affect how input types are processed. + /// + InputOption Input { get; } + + /// + /// Options that affect markdown formatting. + /// + MarkdownOption Markdown { get; } + + /// + /// Options that affect how output is generated. + /// + OutputOption Output { get; } + } + + public sealed class PSDocumentOption : IPSDocumentOption + { + private const string DEFAULT_FILENAME = "ps-docs.yaml"; + + private static readonly PSDocumentOption Default = new() + { + Document = DocumentOption.Default, + Execution = ExecutionOption.Default, + Input = InputOption.Default, + Markdown = MarkdownOption.Default, + Output = OutputOption.Default, + }; + + private string SourcePath; + + public PSDocumentOption() + { + // Set defaults + Configuration = new ConfigurationOption(); + Document = new DocumentOption(); + Execution = new ExecutionOption(); + Input = new InputOption(); + Markdown = new MarkdownOption(); + Output = new OutputOption(); + } + + private PSDocumentOption(string sourcePath, PSDocumentOption option) + { + SourcePath = sourcePath; + + // Set from existing option instance + Configuration = new ConfigurationOption(option?.Configuration); + Document = new DocumentOption(option?.Document); + Execution = new ExecutionOption(option?.Execution); + Input = new InputOption(option?.Input); + Markdown = new MarkdownOption(option?.Markdown); + Output = new OutputOption(option?.Output); + } + + /// + /// A callback that is overridden by PowerShell so that the current working path can be retrieved. + /// + private static PathDelegate _GetWorkingPath = () => Directory.GetCurrentDirectory(); + + /// + /// Sets the current culture to use when processing documents unless otherwise specified. + /// + private static CultureInfo _CurrentCulture = Thread.CurrentThread.CurrentCulture; + + /// + /// Reserved for internal use. + /// + public string Generator { get; set; } + + /// + /// A set of key/ value configuration options for document definitions. + /// + public ConfigurationOption Configuration { get; set; } + + public DocumentOption Document { get; set; } + + /// + /// Options that affect document execution. + /// + public ExecutionOption Execution { get; set; } + + /// + /// Options that affect how input types are processed. + /// + public InputOption Input { get; set; } + + /// + /// Options that affect markdown formatting. + /// + public MarkdownOption Markdown { get; set; } + + /// + /// Options that affect how output is generated. + /// + public OutputOption Output { get; set; } + + public string ToYaml() + { + var s = new SerializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + return s.Serialize(this); + } + + public PSDocumentOption Clone() + { + return new PSDocumentOption(SourcePath, option: this); + } + + private static PSDocumentOption Combine(PSDocumentOption o1, PSDocumentOption o2) + { + var result = new PSDocumentOption(o1?.SourcePath ?? o2?.SourcePath, o1); + result.Configuration = ConfigurationOption.Combine(result.Configuration, o2?.Configuration); + result.Document = DocumentOption.Combine(result.Document, o2?.Document); + result.Execution = ExecutionOption.Combine(result.Execution, o2?.Execution); + result.Input = InputOption.Combine(result.Input, o2?.Input); + result.Markdown = MarkdownOption.Combine(result.Markdown, o2?.Markdown); + result.Output = OutputOption.Combine(result.Output, o2?.Output); + return result; + } + + /// + /// Load a YAML formatted PDocumentOption object from disk. + /// + /// The file or directory path to load options from. + /// When false, if the file does not exist, and exception will be raised. + /// + public static PSDocumentOption FromFile(string path, bool silentlyContinue = false) + { + // Get a rooted file path instead of directory or relative path + var filePath = GetFilePath(path: path); + + // Fallback to defaults even if file does not exist when silentlyContinue is true + if (!File.Exists(filePath)) + { + if (!silentlyContinue) + { + throw new FileNotFoundException(PSDocsResources.OptionsNotFound, filePath); + } + else + { + // Use the default options + return Default.Clone(); + } + } + return FromEnvironment(FromYaml(path: filePath, yaml: File.ReadAllText(filePath))); + } + + public static PSDocumentOption FromDefault() + { + return Default.Clone(); + } + + // + /// Load a YAML formatted PSDocumentOption object from disk. + /// + /// A file or directory to read options from. + /// An options object. + /// + /// This method is called from PowerShell. + /// + public static PSDocumentOption FromFileOrEmpty(string path) + { + // Get a rooted file path instead of directory or relative path + var filePath = GetFilePath(path); + + // Return empty options if file does not exist + if (!File.Exists(filePath)) + return new PSDocumentOption(); + + return FromEnvironment(FromYaml(path: filePath, yaml: File.ReadAllText(filePath))); + } + + /// + /// Load a YAML formatted PSDocumentOption object from disk. + /// + /// + /// A file or directory to read options from. + /// An options object. + /// + /// This method is called from PowerShell. + /// + public static PSDocumentOption FromFileOrEmpty(PSDocumentOption option, string path) + { + if (option == null) + return FromFileOrEmpty(path); + + return !string.IsNullOrEmpty(path) ? Combine(option, FromFileOrEmpty(path)) : option; + } + + public static PSDocumentOption FromYaml(string yaml) + { + var d = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new StringArrayTypeConverter()) + .Build(); + + return d.Deserialize(yaml) ?? new PSDocumentOption(); + } + + public static PSDocumentOption FromYaml(string path, string yaml) + { + var d = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new StringArrayTypeConverter()) + .Build(); + var option = d.Deserialize(yaml) ?? new PSDocumentOption(); + option.SourcePath = path; + return option; + } + + /// + /// Set working path from PowerShell host environment. + /// + /// An $ExecutionContext object. + /// + /// Called from PowerShell. + /// + public static void UseExecutionContext(EngineIntrinsics executionContext) + { + if (executionContext == null) + { + _GetWorkingPath = () => Directory.GetCurrentDirectory(); + return; + } + _GetWorkingPath = () => executionContext.SessionState.Path.CurrentFileSystemLocation.Path; + } + + public static void UseCurrentCulture() + { + UseCurrentCulture(Thread.CurrentThread.CurrentCulture); + } + + public static void UseCurrentCulture(string culture) + { + UseCurrentCulture(CultureInfo.CreateSpecificCulture(culture)); + } + + public static void UseCurrentCulture(CultureInfo culture) + { + _CurrentCulture = culture; + } + + public static string GetWorkingPath() + { + return _GetWorkingPath(); + } + + public static CultureInfo GetCurrentCulture() + { + return _CurrentCulture; + } + + public override bool Equals(object obj) + { + return obj is PSDocumentOption option && Equals(option); + } + + public bool Equals(PSDocumentOption other) + { + return other != null && + Configuration == other.Configuration && + Document == other.Document && + Execution == other.Execution && + Input == other.Input && + Markdown == other.Markdown && + Output == other.Output; + } + + public override int GetHashCode() + { + unchecked // Overflow is fine + { + var hash = 17; + hash = hash * 23 + (Configuration != null ? Configuration.GetHashCode() : 0); + hash = hash * 23 + (Document != null ? Document.GetHashCode() : 0); + hash = hash * 23 + (Execution != null ? Execution.GetHashCode() : 0); + hash = hash * 23 + (Input != null ? Input.GetHashCode() : 0); + hash = hash * 23 + (Markdown != null ? Markdown.GetHashCode() : 0); + hash = hash * 23 + (Output != null ? Output.GetHashCode() : 0); + return hash; + } + } + + /// + /// Convert from hashtable to options by processing key values. This enables -Option @{ } from PowerShell. + /// + /// + public static implicit operator PSDocumentOption(Hashtable hashtable) + { + return FromHashtable(hashtable); + } + + /// + /// Convert from string to options by loading the yaml file from disk. This enables -Option '.\ps-docs.yaml' from PowerShell. + /// + /// + public static implicit operator PSDocumentOption(string path) + { + return FromFile(path); + } + + private static PSDocumentOption FromEnvironment(PSDocumentOption option) + { + if (option == null) + option = new PSDocumentOption(); + + // Start loading matching values + var env = EnvironmentHelper.Default; + //option.Document.Load(env); // Currently set from cmdlet + option.Execution.Load(env); + option.Input.Load(env); + option.Markdown.Load(env); + option.Output.Load(env); + option.Configuration.Load(env); + return option; + } + + public static PSDocumentOption FromHashtable(Hashtable hashtable) + { + var option = new PSDocumentOption(); + if (hashtable == null) + return option; + + // Start loading matching values + var index = BuildIndex(hashtable); + //option.Document.Load(index); // Currently set from cmdlet + option.Execution.Load(index); + option.Input.Load(index); + option.Markdown.Load(index); + option.Output.Load(index); + option.Configuration.Load(index); + return option; + } + + /// + /// Build index to allow mapping values. + /// + [DebuggerStepThrough] + internal static Dictionary BuildIndex(Hashtable hashtable) + { + var index = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in hashtable) + index.Add(entry.Key.ToString(), entry.Value); + + return index; + } + + /// + /// Get a fully qualified file path. + /// + /// A file or directory path. + /// + public static string GetFilePath(string path) + { + var rootedPath = GetRootedPath(path); + if (Path.HasExtension(rootedPath)) + { + var ext = Path.GetExtension(rootedPath); + if (string.Equals(ext, ".yaml", StringComparison.OrdinalIgnoreCase) || string.Equals(ext, ".yml", StringComparison.OrdinalIgnoreCase)) + return rootedPath; + } + + // Check if default files exist and + return UseFilePath(path: rootedPath, name: "ps-docs.yaml") ?? + UseFilePath(path: rootedPath, name: "ps-docs.yml") ?? + UseFilePath(path: rootedPath, name: "psdocs.yaml") ?? + UseFilePath(path: rootedPath, name: "psdocs.yml") ?? + Path.Combine(rootedPath, DEFAULT_FILENAME); + } + + /// + /// Get a full path instead of a relative path that may be passed from PowerShell. + /// + /// + /// + internal static string GetRootedPath(string path) + { + if (string.IsNullOrEmpty(path)) + return Path.GetFullPath(GetWorkingPath()); + + return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(GetWorkingPath(), path)); + } + + /// + /// Get a full path instead of a relative path that may be passed from PowerShell. + /// + internal static string GetRootedBasePath(string path) + { + var rootedPath = GetRootedPath(path); + if (rootedPath.Length > 0 && IsSeparator(rootedPath[rootedPath.Length - 1])) + return rootedPath; + + return string.Concat(rootedPath, Path.DirectorySeparatorChar); + } + + /// + /// Determine if the combined file path is exists. + /// + /// A directory path where a options file may be stored. + /// A file name of an options file. + /// Returns a file path if the file exists or null if the file does not exist. + private static string UseFilePath(string path, string name) + { + var filePath = Path.Combine(path, name); + return File.Exists(filePath) ? filePath : null; + } + + [DebuggerStepThrough] + private static bool IsSeparator(char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Data/CodeContent.cs b/packages/psdocs/src/PSDocs/Data/CodeContent.cs new file mode 100644 index 00000000..67cb5467 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Data/CodeContent.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Data +{ + /// + /// A convertable object for code content. + /// + public sealed class CodeContent + { + public string Content { get; } + + public CodeContentType Type { get; } + + private CodeContent(string content, CodeContentType type) + { + Content = content; + Type = type; + } + + private CodeContent(object value) + { + Type = CodeContentType.Object; + } + + public static implicit operator CodeContent(ScriptBlock content) + { + return FromScriptBlock(content); + } + + public static implicit operator CodeContent(string content) + { + return FromString(content); + } + + public static implicit operator CodeContent(PSObject content) + { + if (content == null) + return null; + + if (content.BaseObject is ScriptBlock sb) + return FromScriptBlock(sb); + + if (content.BaseObject is string s) + return FromString(s); + + return FromObject(content); + } + + public static CodeContent FromScriptBlock(ScriptBlock content) + { + if (content == null) + return null; + + return new CodeContent(content.ToString(), CodeContentType.ScriptBlock); + } + + public static CodeContent FromString(string content) + { + return new CodeContent(content, CodeContentType.String); + } + + public static CodeContent FromObject(object content) + { + return new CodeContent(content); + } + + public enum CodeContentType + { + String, + + ScriptBlock, + + Object + } + } +} diff --git a/packages/psdocs/src/PSDocs/Data/IDocumentBuilder.cs b/packages/psdocs/src/PSDocs/Data/IDocumentBuilder.cs new file mode 100644 index 00000000..8231ba92 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Data/IDocumentBuilder.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; +using PSDocs.Models; +using PSDocs.Processor; +using PSDocs.Runtime; + +namespace PSDocs.Data +{ + internal interface IDocumentBuilder : IDisposable + { + string Name { get; } + + Document Process(RunspaceContext context, PSObject sourceObject); + + void End(RunspaceContext context, IDocumentResult[] completed); + } +} diff --git a/packages/psdocs/src/PSDocs/Data/ITargetInfo.cs b/packages/psdocs/src/PSDocs/Data/ITargetInfo.cs new file mode 100644 index 00000000..3d8908eb --- /dev/null +++ b/packages/psdocs/src/PSDocs/Data/ITargetInfo.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Data +{ + internal interface ITargetInfo + { + string TargetName { get; } + + string TargetType { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Data/InputFileInfo.cs b/packages/psdocs/src/PSDocs/Data/InputFileInfo.cs new file mode 100644 index 00000000..417b43ec --- /dev/null +++ b/packages/psdocs/src/PSDocs/Data/InputFileInfo.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using PSDocs.Configuration; + +namespace PSDocs.Data +{ + public sealed class InputFileInfo : ITargetInfo + { + private readonly string _TargetType; + + internal readonly bool IsUrl; + + internal InputFileInfo(string basePath, string path) + { + if (path.IsUri()) + { + FullName = path; + IsUrl = true; + return; + } + path = PSDocumentOption.GetRootedPath(path); + FullName = path; + BasePath = basePath; + Name = System.IO.Path.GetFileName(path); + Extension = System.IO.Path.GetExtension(path); + DirectoryName = System.IO.Path.GetDirectoryName(path); + Path = ExpressionHelpers.NormalizePath(basePath, FullName); + _TargetType = string.IsNullOrEmpty(Extension) ? System.IO.Path.GetFileNameWithoutExtension(path) : Extension; + } + + public string FullName { get; } + + public string BasePath { get; } + + public string Name { get; } + + public string Extension { get; } + + public string DirectoryName { get; } + + public string Path { get; } + + string ITargetInfo.TargetName => Path; + + string ITargetInfo.TargetType => _TargetType; + + /// + /// Convert to string. + /// + public override string ToString() + { + return FullName; + } + + /// + /// Convert to FileInfo. + /// + public FileInfo AsFileInfo() + { + return new FileInfo(FullName); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Data/Internal/ScriptDocumentBuilder.cs b/packages/psdocs/src/PSDocs/Data/Internal/ScriptDocumentBuilder.cs new file mode 100644 index 00000000..b546a97c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Data/Internal/ScriptDocumentBuilder.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using PSDocs.Definitions; +using PSDocs.Models; +using PSDocs.Processor; +using PSDocs.Runtime; + +namespace PSDocs.Data.Internal +{ + /// + /// Executes a script block to generate a document model. + /// + internal sealed class ScriptDocumentBuilder : IDocumentBuilder + { + private readonly ScriptDocumentBlock _Block; + private readonly IDocumentConvention[] _Conventions; + + private SectionNode _Current; + + [ThreadStatic] + private static Stack _Parent; + + // Track whether Dispose has been called. + private bool _Disposed; + + internal ScriptDocumentBuilder(ScriptDocumentBlock block, IDocumentConvention[] conventions) + { + _Block = block; + _Conventions = conventions; + } + + public string Name => _Block.Name; + + internal Document Document { get; private set; } + + public string Module => _Block.Module; + + Document IDocumentBuilder.Process(RunspaceContext context, PSObject sourceObject) + { + context.EnterBuilder(this); + context.EnterSourceFile(_Block.Source); + try + { + BeginConventions(context, sourceObject); + _Parent = new Stack(); + _Current = Document = new Document(context.DocumentContext); + Document.AddNodes(_Block.Body.Invoke()); + if (Document.Node.Count == 0) + return null; + + ProcessConventions(context, sourceObject); + return Document; + } + catch (Exception e) + { + context.WriteRuntimeException(_Block.SourcePath, e); + return null; + } + finally + { + Document = null; + _Current = null; + _Parent.Clear(); + _Parent = null; + context.ExitBuilder(); + } + } + + void IDocumentBuilder.End(RunspaceContext context, IDocumentResult[] completed) + { + EndConventions(context, completed); + } + + internal void Title(string text) + { + Document.Title = text; + } + + internal void Metadata(IDictionary metadata) + { + foreach (DictionaryEntry kv in metadata) + Document.Metadata[kv.Key] = kv.Value; + } + + internal SectionNode EnterSection(string name) + { + _Parent.Push(_Current); + _Current = new Section + { + Title = name, + Level = _Current.Level + 1, + }; + return _Current; + } + + internal void ExitSection() + { + if (_Parent.Count == 0) + return; + + _Current = _Parent.Pop(); + } + + private void BeginConventions(RunspaceContext context, PSObject sourceObject) + { + if (_Conventions == null || _Conventions.Length == 0) + return; + + try + { + context.PushScope(RunspaceScope.ConventionBegin); + for (var i = 0; i < _Conventions.Length; i++) + { + _Conventions[i].Begin(context, new PSObject[] { sourceObject }); + } + } + finally + { + context.PopScope(); + } + } + + private void ProcessConventions(RunspaceContext context, PSObject sourceObject) + { + if (_Conventions == null || _Conventions.Length == 0) + return; + + try + { + context.PushScope(RunspaceScope.ConventionProcess); + for (var i = 0; i < _Conventions.Length; i++) + { + _Conventions[i].Process(context, new PSObject[] { sourceObject }); + } + } + finally + { + context.PopScope(); + } + } + + private void EndConventions(RunspaceContext context, IEnumerable results) + { + if (_Conventions == null || _Conventions.Length == 0) + return; + + try + { + context.PushScope(RunspaceScope.ConventionEnd); + for (var i = 0; i < _Conventions.Length; i++) + { + _Conventions[i].End(context, results); + } + } + finally + { + context.PopScope(); + } + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + _Block.Dispose(); + Document = null; + _Current = null; + _Parent = null; + } + _Disposed = true; + } + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Conventions/BaseDocumentConvention.cs b/packages/psdocs/src/PSDocs/Definitions/Conventions/BaseDocumentConvention.cs new file mode 100644 index 00000000..dc49de66 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Conventions/BaseDocumentConvention.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using PSDocs.Runtime; + +namespace PSDocs.Definitions.Conventions +{ + internal abstract class BaseDocumentConvention : IDocumentConvention + { + protected BaseDocumentConvention(string name) + { + Name = name; + } + + public string Name { get; } + + public virtual void Begin(RunspaceContext context, IEnumerable input) + { + + } + + public virtual void Process(RunspaceContext context, IEnumerable input) + { + + } + + public virtual void End(RunspaceContext context, IEnumerable input) + { + + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Conventions/DefaultDocumentConvention.cs b/packages/psdocs/src/PSDocs/Definitions/Conventions/DefaultDocumentConvention.cs new file mode 100644 index 00000000..7a114249 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Conventions/DefaultDocumentConvention.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.IO; +using PSDocs.Configuration; +using PSDocs.Runtime; + +namespace PSDocs.Definitions.Conventions +{ + internal sealed class DefaultDocumentConvention : BaseDocumentConvention + { + internal DefaultDocumentConvention(string name) + : base(name) { } + + public override void Process(RunspaceContext context, IEnumerable input) + { + var culture = context.Builder.Document.Culture; + var rootedPath = PSDocumentOption.GetRootedPath(context.Pipeline.Option.Output.Path); + var outputPath = !string.IsNullOrEmpty(culture) && context.Pipeline.Option.Output?.Culture?.Length > 1 ? + Path.Combine(rootedPath, culture) : rootedPath; + + context.DocumentContext.InstanceName = context.Builder.Document.Name; + context.DocumentContext.OutputPath = outputPath; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Conventions/ScriptBlockDocumentConvention.cs b/packages/psdocs/src/PSDocs/Definitions/Conventions/ScriptBlockDocumentConvention.cs new file mode 100644 index 00000000..8b6374d8 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Conventions/ScriptBlockDocumentConvention.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using PSDocs.Pipeline; +using PSDocs.Runtime; + +namespace PSDocs.Definitions.Conventions +{ + internal sealed class ScriptBlockDocumentConvention : BaseDocumentConvention, ILanguageBlock + { + private readonly LanguageScriptBlock _Begin; + private readonly LanguageScriptBlock _Process; + private readonly LanguageScriptBlock _End; + + public ScriptBlockDocumentConvention(SourceFile source, string name, LanguageScriptBlock begin, LanguageScriptBlock process, LanguageScriptBlock end) + : base(name) + { + Source = source; + Id = ResourceHelper.GetId(source.ModuleName, name); + _Begin = begin; + _Process = process; + _End = end; + } + + public string Id { get; } + + public SourceFile Source { get; } + + string ILanguageBlock.Module => Source.ModuleName; + + string ILanguageBlock.SourcePath => Source.Path; + + public override void Begin(RunspaceContext context, IEnumerable input) + { + InvokeConventionBlock(_Begin, input); + } + + public override void Process(RunspaceContext context, IEnumerable input) + { + InvokeConventionBlock(_Process, input); + } + + public override void End(RunspaceContext context, IEnumerable input) + { + InvokeConventionBlock(_End, input); + } + + private void InvokeConventionBlock(LanguageScriptBlock block, IEnumerable input) + { + if (block == null) + return; + + try + { + block.Invoke(); + } + finally + { + + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/IAnnotated.cs b/packages/psdocs/src/PSDocs/Definitions/IAnnotated.cs new file mode 100644 index 00000000..b64fff73 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/IAnnotated.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Definitions +{ + internal interface IAnnotated + { + TAnnotation GetAnnotation() where TAnnotation : T; + + void SetAnnotation(TAnnotation annotation) where TAnnotation : T; + } + + internal static class AnnotatedExtensions + { + internal static TAnnotation RequireAnnotation(this IAnnotated annotated) where TAnnotation : T, new() + { + var result = annotated.GetAnnotation(); + return result == null ? new TAnnotation() : result; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/IDocumentConvention.cs b/packages/psdocs/src/PSDocs/Definitions/IDocumentConvention.cs new file mode 100644 index 00000000..88303fd7 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/IDocumentConvention.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using PSDocs.Runtime; + +namespace PSDocs.Definitions +{ + internal interface IDocumentConvention + { + string Name { get; } + + void Begin(RunspaceContext context, IEnumerable input); + + void Process(RunspaceContext context, IEnumerable input); + + void End(RunspaceContext context, IEnumerable input); + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/IDocumentDefinition.cs b/packages/psdocs/src/PSDocs/Definitions/IDocumentDefinition.cs new file mode 100644 index 00000000..0b4b67fc --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/IDocumentDefinition.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Runtime; + +namespace PSDocs.Definitions +{ + public interface IDocumentDefinition : ILanguageBlock + { + + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Resource.cs b/packages/psdocs/src/PSDocs/Definitions/Resource.cs new file mode 100644 index 00000000..ce2cee5b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Resource.cs @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using PSDocs.Pipeline; +using PSDocs.Runtime; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; +using YamlDotNet.Serialization.NamingConventions; +using YamlDotNet.Serialization.NodeDeserializers; + +namespace PSDocs.Definitions +{ + public enum ResourceKind + { + None = 0, + + /// + /// A selector resource. + /// + Selector = 1 + } + + internal interface IResource : ILanguageBlock + { + ResourceKind Kind { get; } + + string ApiVersion { get; } + + string Name { get; } + } + + internal abstract class ResourceRef + { + public readonly string Id; + public readonly ResourceKind Kind; + + protected ResourceRef(string id, ResourceKind kind) + { + Kind = kind; + Id = id; + } + } + + internal abstract class ResourceAnnotation + { + + } + + internal sealed class ValidateResourceAnnotation : ResourceAnnotation + { + public bool ApiVersionNotSet { get; internal set; } + } + + public sealed class ResourceObject + { + internal ResourceObject(IResource block) + { + Block = block; + } + + internal IResource Block { get; } + } + + internal sealed class ResourceBuilder + { + private readonly List _Output; + private readonly IDeserializer _Deserializer; + + internal ResourceBuilder() + { + _Output = new List(); + _Deserializer = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .WithTypeConverter(new FieldMapYamlTypeConverter()) + .WithNodeDeserializer( + inner => new LanguageBlockDeserializer(new SelectorExpressionDeserializer(inner)), + s => s.InsteadOf()) + .Build(); + } + + internal void FromFile(SourceFile file) + { + using var reader = new StreamReader(file.Path); + var parser = new Parser(reader); + parser.TryConsume(out _); + while (parser.Current is DocumentStart) + { + var item = _Deserializer.Deserialize(parser: parser); + if (item == null || item.Block == null) + continue; + + _Output.Add(item.Block); + } + reader.Close(); + } + + internal IEnumerable Build() + { + return _Output.Count == 0 ? Array.Empty() : _Output.ToArray(); + } + } + + public sealed class ResourceAnnotations : Dictionary + { + + } + + public sealed class ResourceMetadata + { + public ResourceMetadata() + { + Annotations = new ResourceAnnotations(); + } + + public string Name { get; set; } + + public ResourceAnnotations Annotations { get; set; } + } + + public sealed class ResourceExtent + { + public string File { get; set; } + + public string Module { get; set; } + } + + public sealed class ResourceHelpInfo + { + internal ResourceHelpInfo(string synopsis) + { + Synopsis = synopsis; + } + + public string Synopsis { get; private set; } + } + + public abstract class Resource where TSpec : Spec, new() + { + protected Resource(ResourceKind kind, string apiVersion, SourceFile source, ResourceMetadata metadata, ResourceHelpInfo info, TSpec spec) + { + Kind = kind; + ApiVersion = apiVersion; + Info = info; + Source = source; + Spec = spec; + Id = ResourceHelper.GetId(source.ModuleName, metadata.Name); + Metadata = metadata; + Name = metadata.Name; + } + + [YamlIgnore()] + public readonly string Id; + + [YamlIgnore()] + public string Name { get; } + + /// + /// The file path where the resource is defined. + /// + [YamlIgnore()] + public readonly SourceFile Source; + + public readonly ResourceHelpInfo Info; + + public ResourceMetadata Metadata { get; } + + public ResourceKind Kind { get; } + + public string ApiVersion { get; } + + public TSpec Spec { get; } + } + + public abstract class InternalResource : Resource, IResource, IAnnotated where TSpec : Spec, new() + { + private readonly Dictionary _Annotations; + + internal InternalResource(ResourceKind kind, string apiVersion, SourceFile source, ResourceMetadata metadata, ResourceHelpInfo info, TSpec spec) + : base(kind, apiVersion, source, metadata, info, spec) + { + _Annotations = new Dictionary(); + } + + string ILanguageBlock.Id => Id; + + string ILanguageBlock.SourcePath => Source.Path; + + string ILanguageBlock.Module => Source.ModuleName; + + ResourceKind IResource.Kind => Kind; + + string IResource.ApiVersion => ApiVersion; + + string IResource.Name => Name; + + TAnnotation IAnnotated.GetAnnotation() + { + return _Annotations.TryGetValue(typeof(TAnnotation), out var annotation) ? (TAnnotation)annotation : null; + } + + void IAnnotated.SetAnnotation(TAnnotation annotation) + { + _Annotations[typeof(TAnnotation)] = annotation; + } + } + + internal static class ResourceHelper + { + private const string ANNOTATION_OBSOLETE = "obsolete"; + private const string LooseModuleName = "."; + private const char ModuleSeparator = '\\'; + + internal static bool IsObsolete(ResourceMetadata metadata) + { + if (metadata == null || metadata.Annotations == null || !metadata.Annotations.TryGetBool(ANNOTATION_OBSOLETE, out var obsolete)) + return false; + + return obsolete.GetValueOrDefault(false); + } + + internal static string GetId(string moduleName, string name) + { + if (name.Contains("\\")) + return name; + + return string.Concat(string.IsNullOrEmpty(moduleName) ? LooseModuleName : moduleName, ModuleSeparator, name); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Selectors/Selector.cs b/packages/psdocs/src/PSDocs/Definitions/Selectors/Selector.cs new file mode 100644 index 00000000..5eba5009 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Selectors/Selector.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using PSDocs.Pipeline; + +namespace PSDocs.Definitions.Selectors +{ + [Spec(Specs.V1, Specs.Selector)] + internal sealed class SelectorV1 : InternalResource + { + public SelectorV1(string apiVersion, SourceFile source, ResourceMetadata metadata, ResourceHelpInfo info, SelectorV1Spec spec) + : base(ResourceKind.Selector, apiVersion, source, metadata, info, spec) { } + } + + internal sealed class SelectorV1Spec : Spec + { + public SelectorIf If { get; set; } + } + + internal abstract class SelectorExpression + { + public SelectorExpression(SelectorExpresssionDescriptor descriptor) + { + Descriptor = descriptor; + } + + internal sealed class PropertyBag : KeyMapDictionary + { + public PropertyBag() + : base() { } + } + + public SelectorExpresssionDescriptor Descriptor { get; } + } + + [DebuggerDisplay("Selector If")] + internal sealed class SelectorIf : SelectorExpression + { + public SelectorIf(SelectorExpression expression) + : base(null) + { + Expression = expression; + } + + public SelectorExpression Expression { get; set; } + } + + [DebuggerDisplay("Selector {Descriptor.Name}")] + internal sealed class SelectorOperator : SelectorExpression + { + internal SelectorOperator(SelectorExpresssionDescriptor descriptor) + : base(descriptor) + { + Children = new List(); + } + + public List Children { get; } + + public void Add(SelectorExpression item) + { + Children.Add(item); + } + } + + [DebuggerDisplay("Selector {Descriptor.Name}")] + internal sealed class SelectorCondition : SelectorExpression + { + internal SelectorCondition(SelectorExpresssionDescriptor descriptor, PropertyBag properties) + : base(descriptor) + { + Property = properties ?? new PropertyBag(); + } + + public PropertyBag Property { get; } + + internal void Add(PropertyBag properties) + { + Property.AddUnique(properties); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorContext.cs b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorContext.cs new file mode 100644 index 00000000..68c3c54b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorContext.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using PSDocs.Runtime; + +namespace PSDocs.Definitions.Selectors +{ + internal class SelectorContext : IBindingContext + { + private readonly Dictionary _NameTokenCache; + + internal SelectorContext() + { + _NameTokenCache = new Dictionary(); + } + + void IBindingContext.CacheNameToken(string expression, NameToken nameToken) + { + _NameTokenCache[expression] = nameToken; + } + + bool IBindingContext.GetNameToken(string expression, out NameToken nameToken) + { + return _NameTokenCache.TryGetValue(expression, out nameToken); + } + + internal void Debug(string message, params object[] args) + { + if (RunspaceContext.CurrentThread?.Pipeline?.Writer == null) + return; + + RunspaceContext.CurrentThread.Pipeline.Writer.WriteDebug(message, args); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs new file mode 100644 index 00000000..539de5ab --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorExpressions.cs @@ -0,0 +1,638 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using PSDocs.Pipeline; +using PSDocs.Resources; + +namespace PSDocs.Definitions.Selectors +{ + internal delegate bool SelectorExpressionFn(SelectorContext context, SelectorInfo info, object[] args, object o); + + internal delegate bool SelectorExpressionOuterFn(SelectorContext context, object o); + + internal enum SelectorExpressionType + { + Operator = 1, + + Condition = 2 + } + + internal interface ISelectorExpresssionDescriptor + { + string Name { get; } + + SelectorExpressionType Type { get; } + + SelectorExpression CreateInstance(SourceFile source, SelectorExpression.PropertyBag properties); + } + + internal sealed class SelectorExpresssionDescriptor : ISelectorExpresssionDescriptor + { + public SelectorExpresssionDescriptor(string name, SelectorExpressionType type, SelectorExpressionFn fn) + { + Name = name; + Type = type; + Fn = fn; + } + + public string Name { get; } + + public SelectorExpressionType Type { get; } + + public SelectorExpressionFn Fn { get; } + + public SelectorExpression CreateInstance(SourceFile source, SelectorExpression.PropertyBag properties) + { + if (Type == SelectorExpressionType.Operator) + return new SelectorOperator(this); + + if (Type == SelectorExpressionType.Condition) + return new SelectorCondition(this, properties); + + return null; + } + } + + internal sealed class SelectorInfo + { + private readonly string path; + + public SelectorInfo(string path) + { + this.path = path; + } + } + + internal sealed class SelectorExpressionFactory + { + private readonly Dictionary _Descriptors; + + public SelectorExpressionFactory() + { + _Descriptors = new Dictionary(SelectorExpressions.Builtin.Length, StringComparer.OrdinalIgnoreCase); + foreach (var d in SelectorExpressions.Builtin) + With(d); + } + + public bool TryDescriptor(string name, out ISelectorExpresssionDescriptor descriptor) + { + return _Descriptors.TryGetValue(name, out descriptor); + } + + public bool IsOperator(string name) + { + return TryDescriptor(name, out var d) && d != null && d.Type == SelectorExpressionType.Operator; + } + + public bool IsCondition(string name) + { + return TryDescriptor(name, out var d) && d != null && d.Type == SelectorExpressionType.Condition; + } + + private void With(ISelectorExpresssionDescriptor descriptor) + { + _Descriptors.Add(descriptor.Name, descriptor); + } + } + + internal sealed class SelectorExpressionBuilder + { + private const char Dot = '.'; + private const char OpenBracket = '['; + private const char CloseBracket = '['; + + private readonly bool _Debugger; + + public SelectorExpressionBuilder(bool debugger = true) + { + _Debugger = debugger; + } + + public SelectorExpressionOuterFn Build(SelectorIf selectorIf) + { + return Expression(string.Empty, selectorIf.Expression); + } + + private SelectorExpressionOuterFn Expression(string path, SelectorExpression expression) + { + path = Path(path, expression); + if (expression is SelectorOperator selectorOperator) + return Debugger(Operator(path, selectorOperator), path); + else if (expression is SelectorCondition selectorCondition) + return Debugger(Condition(path, selectorCondition), path); + + throw new InvalidOperationException(); + } + + private static SelectorExpressionOuterFn Condition(string path, SelectorCondition expression) + { + var info = new SelectorInfo(path); + return (context, o) => expression.Descriptor.Fn(context, info, new object[] { expression.Property }, o); + } + + private static string Path(string path, SelectorExpression expression) + { + path = string.Concat(path, Dot, expression.Descriptor.Name); + return path; + } + + private SelectorExpressionOuterFn Operator(string path, SelectorOperator expression) + { + var inner = new List(expression.Children.Count); + for (var i = 0; i < expression.Children.Count; i++) + { + var childPath = string.Concat(path, OpenBracket, i, CloseBracket); + inner.Add(Expression(childPath, expression.Children[i])); + } + var innerA = inner.ToArray(); + var info = new SelectorInfo(path); + return (context, o) => expression.Descriptor.Fn(context, info, innerA, o); + } + + private SelectorExpressionOuterFn Debugger(SelectorExpressionOuterFn expression, string path) + { + if (!_Debugger) + return expression; + + return (context, o) => DebuggerFn(context, path, expression, o); + } + + private static bool DebuggerFn(SelectorContext context, string path, SelectorExpressionOuterFn expression, object o) + { + var result = expression(context, o); + context.Debug(PSDocsResources.SelectorTrace, path, result); + return result; + } + } + + /// + /// Expressions that can be used with selectors. + /// + internal sealed class SelectorExpressions + { + // Conditions + private const string EXISTS = "exists"; + private const string EQUALS = "equals"; + private const string NOTEQUALS = "notEquals"; + private const string HASVALUE = "hasValue"; + private const string MATCH = "match"; + private const string NOTMATCH = "notMatch"; + private const string IN = "in"; + private const string NOTIN = "notIn"; + private const string LESS = "less"; + private const string LESSOREQUALS = "lessOrEquals"; + private const string GREATER = "greater"; + private const string GREATEROREQUALS = "greaterOrEquals"; + private const string STARTSWITH = "startsWith"; + private const string ENDSWITH = "endsWith"; + private const string CONTAINS = "contains"; + private const string ISSTRING = "isString"; + private const string ISLOWER = "isLower"; + private const string ISUPPER = "isUpper"; + + // Operators + private const string IF = "if"; + private const string ANYOF = "anyOf"; + private const string ALLOF = "allOf"; + private const string NOT = "not"; + private const string FIELD = "field"; + + // Define built-ins + internal static readonly ISelectorExpresssionDescriptor[] Builtin = new ISelectorExpresssionDescriptor[] + { + // Operators + new SelectorExpresssionDescriptor(IF, SelectorExpressionType.Operator, If), + new SelectorExpresssionDescriptor(ANYOF, SelectorExpressionType.Operator, AnyOf), + new SelectorExpresssionDescriptor(ALLOF, SelectorExpressionType.Operator, AllOf), + new SelectorExpresssionDescriptor(NOT, SelectorExpressionType.Operator, Not), + + // Conditions + new SelectorExpresssionDescriptor(EXISTS, SelectorExpressionType.Condition, Exists), + new SelectorExpresssionDescriptor(EQUALS, SelectorExpressionType.Condition, Equals), + new SelectorExpresssionDescriptor(NOTEQUALS, SelectorExpressionType.Condition, NotEquals), + new SelectorExpresssionDescriptor(HASVALUE, SelectorExpressionType.Condition, HasValue), + new SelectorExpresssionDescriptor(MATCH, SelectorExpressionType.Condition, Match), + new SelectorExpresssionDescriptor(NOTMATCH, SelectorExpressionType.Condition, NotMatch), + new SelectorExpresssionDescriptor(IN, SelectorExpressionType.Condition, In), + new SelectorExpresssionDescriptor(NOTIN, SelectorExpressionType.Condition, NotIn), + new SelectorExpresssionDescriptor(LESS, SelectorExpressionType.Condition, Less), + new SelectorExpresssionDescriptor(LESSOREQUALS, SelectorExpressionType.Condition, LessOrEquals), + new SelectorExpresssionDescriptor(GREATER, SelectorExpressionType.Condition, Greater), + new SelectorExpresssionDescriptor(GREATEROREQUALS, SelectorExpressionType.Condition, GreaterOrEquals), + new SelectorExpresssionDescriptor(STARTSWITH, SelectorExpressionType.Condition, StartsWith), + new SelectorExpresssionDescriptor(ENDSWITH, SelectorExpressionType.Condition, EndsWith), + new SelectorExpresssionDescriptor(CONTAINS, SelectorExpressionType.Condition, Contains), + new SelectorExpresssionDescriptor(ISSTRING, SelectorExpressionType.Condition, IsString), + new SelectorExpresssionDescriptor(ISLOWER, SelectorExpressionType.Condition, IsLower), + new SelectorExpresssionDescriptor(ISUPPER, SelectorExpressionType.Condition, IsUpper), + }; + + #region Operators + + internal static bool If(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var inner = GetInner(args); + if (inner.Length > 0) + return inner[0](context, o); + + return false; + } + + internal static bool AnyOf(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var inner = GetInner(args); + for (var i = 0; i < inner.Length; i++) + { + if (inner[i](context, o)) + return true; + } + return false; + } + + internal static bool AllOf(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var inner = GetInner(args); + for (var i = 0; i < inner.Length; i++) + { + if (!inner[i](context, o)) + return false; + } + return true; + } + + internal static bool Not(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var inner = GetInner(args); + if (inner.Length > 0) + return !inner[0](context, o); + + return false; + } + + #endregion Operators + + #region Conditions + + internal static bool Exists(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, EXISTS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, EXISTS, field, propertyValue); + return propertyValue == ExpressionHelpers.Exists(context, o, field, caseSensitive: false); + } + return false; + } + + internal static bool Equals(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryProperty(properties, EQUALS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, EQUALS, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return false; + + // int, string, bool + return ExpressionHelpers.Equal(propertyValue, value, caseSensitive: false, convertExpected: true); + } + return false; + } + + internal static bool NotEquals(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryProperty(properties, NOTEQUALS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, NOTEQUALS, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + // int, string, bool + return !ExpressionHelpers.Equal(propertyValue, value, caseSensitive: false, convertExpected: true); + } + return false; + } + + internal static bool HasValue(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, HASVALUE, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, HASVALUE, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return !propertyValue.Value; + + return !propertyValue.Value == ExpressionHelpers.NullOrEmpty(value); + } + return false; + } + + internal static bool Match(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryProperty(properties, MATCH, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, MATCH, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return false; + + return ExpressionHelpers.Match(propertyValue, value, caseSensitive: false); + } + return false; + } + + internal static bool NotMatch(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryProperty(properties, NOTMATCH, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, NOTMATCH, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + return !ExpressionHelpers.Match(propertyValue, value, caseSensitive: false); + } + return false; + } + + internal static bool In(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyArray(properties, IN, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, IN, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.AnyValue(value, propertyValue.GetValue(i), caseSensitive: false, out _)) + return true; + } + return false; + } + return false; + } + + internal static bool NotIn(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyArray(properties, NOTIN, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, NOTIN, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.AnyValue(value, propertyValue.GetValue(i), caseSensitive: false, out _)) + return false; + } + return true; + } + return false; + } + + internal static bool Less(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyLong(properties, LESS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, LESS, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + if (value == null) + return 0 < propertyValue; + + if (ExpressionHelpers.CompareNumeric(value, propertyValue, convert: false, compare: out var compare, value: out _)) + return compare < 0; + } + return false; + } + + internal static bool LessOrEquals(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyLong(properties, LESSOREQUALS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, LESSOREQUALS, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + if (value == null) + return 0 <= propertyValue; + + if (ExpressionHelpers.CompareNumeric(value, propertyValue, convert: false, compare: out var compare, value: out _)) + return compare <= 0; + } + return false; + } + + internal static bool Greater(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyLong(properties, GREATER, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, GREATER, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + if (value == null) + return 0 > propertyValue; + + if (ExpressionHelpers.CompareNumeric(value, propertyValue, convert: false, compare: out var compare, value: out _)) + return compare > 0; + } + return false; + } + + internal static bool GreaterOrEquals(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyLong(properties, GREATEROREQUALS, out var propertyValue) && TryField(properties, out var field)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, GREATEROREQUALS, field, propertyValue); + if (!ObjectHelper.GetField(context, o, field, caseSensitive: false, out var value)) + return true; + + if (value == null) + return 0 >= propertyValue; + + if (ExpressionHelpers.CompareNumeric(value, propertyValue, convert: false, compare: out var compare, value: out _)) + return compare >= 0; + } + return false; + } + + internal static bool StartsWith(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, STARTSWITH, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, STARTSWITH, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out var value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.StartsWith(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool EndsWith(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, ENDSWITH, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, ENDSWITH, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out var value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.EndsWith(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool Contains(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyStringArray(properties, CONTAINS, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, CONTAINS, operand, propertyValue); + if (!ExpressionHelpers.TryString(operand, out var value)) + return false; + + for (var i = 0; propertyValue != null && i < propertyValue.Length; i++) + { + if (ExpressionHelpers.Contains(value, propertyValue[i], caseSensitive: false)) + return true; + } + return false; + } + return false; + } + + internal static bool IsString(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISSTRING, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + context.Debug(PSDocsResources.SelectorExpressionTrace, ISSTRING, operand, propertyValue); + return propertyValue == ExpressionHelpers.TryString(operand, out _); + } + return false; + } + + internal static bool IsLower(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISLOWER, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + if (!ExpressionHelpers.TryString(operand, out var value)) + return !propertyValue.Value; + + context.Debug(PSDocsResources.SelectorExpressionTrace, ISLOWER, operand, propertyValue); + return propertyValue == ExpressionHelpers.IsLower(value, requireLetters: false, notLetter: out _); + } + return false; + } + + internal static bool IsUpper(SelectorContext context, SelectorInfo info, object[] args, object o) + { + var properties = GetProperties(args); + if (TryPropertyBool(properties, ISUPPER, out var propertyValue) && TryOperand(context, o, properties, out var operand)) + { + if (!ExpressionHelpers.TryString(operand, out var value)) + return !propertyValue.Value; + + context.Debug(PSDocsResources.SelectorExpressionTrace, ISUPPER, operand, propertyValue); + return propertyValue == ExpressionHelpers.IsUpper(value, requireLetters: false, notLetter: out _); + } + return false; + } + + #endregion Conditions + + #region Helper methods + + private static bool TryProperty(SelectorExpression.PropertyBag properties, string propertyName, out object propertyValue) + { + return properties.TryGetValue(propertyName, out propertyValue); + } + + private static bool TryPropertyBool(SelectorExpression.PropertyBag properties, string propertyName, out bool? propertyValue) + { + return properties.TryGetBool(propertyName, out propertyValue); + } + + private static bool TryPropertyLong(SelectorExpression.PropertyBag properties, string propertyName, out long? propertyValue) + { + return properties.TryGetLong(propertyName, out propertyValue); + } + + private static bool TryField(SelectorExpression.PropertyBag properties, out string field) + { + return properties.TryGetString(FIELD, out field); + } + + private static bool TryOperand(SelectorContext context, object o, SelectorExpression.PropertyBag properties, out object operand) + { + operand = null; + if (properties.TryGetString(FIELD, out var field)) + return ObjectHelper.GetField(context, o, field, caseSensitive: false, out operand); + + return false; + } + + private static bool TryPropertyArray(SelectorExpression.PropertyBag properties, string propertyName, out Array propertyValue) + { + if (properties.TryGetValue(propertyName, out var array) && array is Array arrayValue) + { + propertyValue = arrayValue; + return true; + } + propertyValue = null; + return false; + } + + private static bool TryPropertyStringArray(SelectorExpression.PropertyBag properties, string propertyName, out string[] propertyValue) + { + if (properties.TryGetStringArray(propertyName, out propertyValue)) + { + return true; + } + else if (properties.TryGetString(propertyName, out var s)) + { + propertyValue = new string[] { s }; + return true; + } + propertyValue = null; + return false; + } + + private static SelectorExpression.PropertyBag GetProperties(object[] args) + { + return (SelectorExpression.PropertyBag)args[0]; + } + + private static SelectorExpressionOuterFn[] GetInner(object[] args) + { + return (SelectorExpressionOuterFn[])args; + } + + #endregion Helper methods + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorVisitor.cs b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorVisitor.cs new file mode 100644 index 00000000..2db70a83 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Selectors/SelectorVisitor.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using PSDocs.Resources; + +namespace PSDocs.Definitions.Selectors +{ + [DebuggerDisplay("Id: {Id}")] + internal sealed class SelectorVisitor + { + private readonly SelectorExpressionOuterFn _Fn; + + public SelectorVisitor(string id, SelectorIf expression) + { + Id = id; + InstanceId = Guid.NewGuid(); + var builder = new SelectorExpressionBuilder(); + _Fn = builder.Build(expression); + } + + public Guid InstanceId { get; } + + public string Id { get; } + + public bool Match(object o) + { + var context = new SelectorContext(); + context.Debug(PSDocsResources.SelectorMatchTrace, Id); + return _Fn(context, o); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/Spec.cs b/packages/psdocs/src/PSDocs/Definitions/Spec.cs new file mode 100644 index 00000000..30c44682 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/Spec.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Definitions.Selectors; + +namespace PSDocs.Definitions +{ + public abstract class Spec + { + private const string FullNameSeparator = "/"; + + public static string GetFullName(string apiVersion, string name) + { + return string.Concat(apiVersion, FullNameSeparator, name); + } + } + + internal static class Specs + { + internal const string V1 = "github.com/microsoft/PSDocs/v1"; + internal const string Selector = "Selector"; + + public static readonly ISpecDescriptor[] BuiltinTypes = new ISpecDescriptor[] + { + new SpecDescriptor(V1, Selector), + }; + } +} diff --git a/packages/psdocs/src/PSDocs/Definitions/SpecFactory.cs b/packages/psdocs/src/PSDocs/Definitions/SpecFactory.cs new file mode 100644 index 00000000..149e839d --- /dev/null +++ b/packages/psdocs/src/PSDocs/Definitions/SpecFactory.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using PSDocs.Annotations; +using PSDocs.Pipeline; + +namespace PSDocs.Definitions +{ + internal sealed class SpecFactory + { + private readonly Dictionary _Descriptors; + + public SpecFactory() + { + _Descriptors = new Dictionary(); + foreach (var d in Specs.BuiltinTypes) + With(d); + } + + public bool TryDescriptor(string apiVersion, string name, out ISpecDescriptor descriptor) + { + var fullName = Spec.GetFullName(apiVersion, name); + return _Descriptors.TryGetValue(fullName, out descriptor); + } + + public void With(string name, string apiVersion) where T : Resource, IResource where TSpec : Spec, new() + { + var descriptor = new SpecDescriptor(name, apiVersion); + _Descriptors.Add(descriptor.FullName, descriptor); + } + + private void With(ISpecDescriptor descriptor) + { + _Descriptors.Add(descriptor.FullName, descriptor); + } + } + + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)] + internal sealed class SpecAttribute : Attribute + { + public SpecAttribute() + { + + } + + public SpecAttribute(string apiVersion, string kind) + { + ApiVersion = apiVersion; + Kind = kind; + } + + public string ApiVersion { get; } + + public string Kind { get; } + } + + internal sealed class SpecDescriptor : ISpecDescriptor where T : Resource, IResource where TSpec : Spec, new() + { + public SpecDescriptor(string apiVersion, string name) + { + ApiVersion = apiVersion; + Name = name; + FullName = Spec.GetFullName(apiVersion, name); + } + + public string Name { get; } + + public string ApiVersion { get; } + + public string FullName { get; } + + public Type SpecType => typeof(TSpec); + + public IResource CreateInstance(SourceFile source, ResourceMetadata metadata, CommentMetadata comment, object spec) + { + var info = new ResourceHelpInfo(comment.Synopsis); + return (IResource)Activator.CreateInstance(typeof(T), ApiVersion, source, metadata, info, spec); + } + } + + internal interface ISpecDescriptor + { + string Name { get; } + + string ApiVersion { get; } + + string FullName { get; } + + Type SpecType { get; } + + IResource CreateInstance(SourceFile source, ResourceMetadata metadata, CommentMetadata comment, object spec); + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Alignment.cs b/packages/psdocs/src/PSDocs/Models/Alignment.cs new file mode 100644 index 00000000..d9da9124 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Alignment.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public enum Alignment + { + Center, + + Left, + + Right, + + Undefined + } +} \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/Models/BlockQuote.cs b/packages/psdocs/src/PSDocs/Models/BlockQuote.cs new file mode 100644 index 00000000..eb7d1e00 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/BlockQuote.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public sealed class BlockQuote : DocumentNode + { + public override DocumentNodeType Type => DocumentNodeType.BlockQuote; + + public string Info { get; set; } + + public string Title { get; set; } + + public string[] Content { get; set; } + } +} \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/Models/Code.cs b/packages/psdocs/src/PSDocs/Models/Code.cs new file mode 100644 index 00000000..e4c0ec4a --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Code.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public sealed class Code : DocumentNode + { + public override DocumentNodeType Type => DocumentNodeType.Code; + + public string Content { get; set; } + + public string Info { get; set; } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Document.cs b/packages/psdocs/src/PSDocs/Models/Document.cs new file mode 100644 index 00000000..cebbd698 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Document.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Specialized; +using PSDocs.Runtime; + +namespace PSDocs.Models +{ + public sealed class Document : SectionNode + { + internal readonly DocumentContext Context; + + internal Document(DocumentContext context) + { + Context = context; + } + + public string Name => Context?.InstanceName; + + public string Culture => Context?.Culture; + + public override DocumentNodeType Type => DocumentNodeType.Document; + + public OrderedDictionary Metadata => Context?.Metadata; + + public Hashtable Data => Context?.Data; + + public string Path => Context?.OutputPath; + } +} diff --git a/packages/psdocs/src/PSDocs/Models/DocumentFilter.cs b/packages/psdocs/src/PSDocs/Models/DocumentFilter.cs new file mode 100644 index 00000000..13449e42 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/DocumentFilter.cs @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace PSDocs.Models +{ + public sealed class DocumentFilter + { + private readonly HashSet _AcceptedNames; + private readonly HashSet _RequiredTags; + + private DocumentFilter(string[] name, string[] tag) + { + _AcceptedNames = new HashSet(name, StringComparer.OrdinalIgnoreCase); + _RequiredTags = new HashSet(tag, StringComparer.OrdinalIgnoreCase); + } + + public static DocumentFilter Create(string[] name, string[] tag) + { + return new DocumentFilter( + name: name ?? Array.Empty(), + tag: tag ?? Array.Empty() + ); + } + + public bool Match(string name, string[] tag) + { + // If name is filtered, the name must be listed + if (_AcceptedNames.Count > 0 && !_AcceptedNames.Contains(name)) + { + return false; + } + + // Check if no tags are required + if (_RequiredTags.Count == 0) + { + return true; + } + + // Check for impossible match + if (tag == null || _RequiredTags.Count > tag.Length) + { + return false; + } + + // Check each tag + foreach (var t in tag) + { + if (!_RequiredTags.Contains(t)) + { + return false; + } + } + + return true; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/DocumentNode.cs b/packages/psdocs/src/PSDocs/Models/DocumentNode.cs new file mode 100644 index 00000000..953e1338 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/DocumentNode.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public abstract class DocumentNode + { + public abstract DocumentNodeType Type { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/DocumentNodeType.cs b/packages/psdocs/src/PSDocs/Models/DocumentNodeType.cs new file mode 100644 index 00000000..4315b9bf --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/DocumentNodeType.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public enum DocumentNodeType + { + Document, + + Section, + + Table, + + Code, + + BlockQuote, + + Text, + + Include + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Include.cs b/packages/psdocs/src/PSDocs/Models/Include.cs new file mode 100644 index 00000000..ea7aaac4 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Include.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; + +namespace PSDocs.Models +{ + public sealed class Include : DocumentNode + { + private string _Path; + + public override DocumentNodeType Type => DocumentNodeType.Include; + + public string Path + { + get => _Path; + set + { + _Path = value; + Exists = File.Exists(_Path); + } + } + + public string Content { get; set; } + + internal bool Exists { get; private set; } + + public override string ToString() + { + return Content; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/ModelHelper.cs b/packages/psdocs/src/PSDocs/Models/ModelHelper.cs new file mode 100644 index 00000000..54ff952c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/ModelHelper.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.IO; +using PSDocs.Configuration; + +namespace PSDocs.Models +{ + internal static class ModelHelper + { + public static Document NewDocument() + { + return new Document(null); + } + + public static Section NewSection(string name, int level) + { + return new Section + { + Title = name, + Level = level + }; + } + + public static TableBuilder Table() + { + return new TableBuilder(); + } + + public static Code NewCode() + { + return new Code + { + + }; + } + + public static BlockQuote BlockQuote(string info, string title) + { + return new BlockQuote + { + Info = info, + Title = title + }; + } + + public static Text Text(string value) + { + return new Text + { + Content = value + }; + } + + public static Include Include(string baseDirectory, string culture, string fileName, bool useCulture) + { + return Include(baseDirectory, culture, fileName, useCulture, null); + } + + internal static Include Include(string baseDirectory, string culture, string fileName, bool useCulture, IDictionary replace) + { + baseDirectory = PSDocumentOption.GetRootedPath(baseDirectory); + var absolutePath = Path.IsPathRooted(fileName) ? fileName : Path.Combine(baseDirectory, (useCulture ? culture : string.Empty), fileName); + var result = new Include + { + Path = absolutePath + }; + if (result.Exists) + { + var text = File.ReadAllText(absolutePath); + if (replace != null && replace.Count > 0) + { + foreach (var key in replace.Keys) + { + var k = key?.ToString(); + var v = replace[key]?.ToString(); + text = text.Replace(k, v); + } + } + result.Content = text; + } + return result; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/PSDocsContext.cs b/packages/psdocs/src/PSDocs/Models/PSDocsContext.cs new file mode 100644 index 00000000..32f61bd4 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/PSDocsContext.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.IO; +using PSDocs.Configuration; + +namespace PSDocs.Models +{ + public delegate object WriteDocumentDelegate(PSDocumentOption option, Document document); + + public sealed class PSDocsContext + { + public PSDocumentOption Option { get; private set; } + + public DocumentFilter Filter { get; private set; } + + public string OutputPath { get; set; } + + public string[] InstanceName { get; set; } + + public string[] Culture { get; set; } + + public WriteDocumentDelegate WriteDocumentHook { get; set; } + + public static PSDocsContext Create(PSDocumentOption option, string[] name, string[] tag, string outputPath) + { + var actualPath = outputPath; + + if (!Path.IsPathRooted(actualPath)) + { + actualPath = Path.Combine(PSDocumentOption.GetWorkingPath(), outputPath); + } + + return new PSDocsContext + { + Option = option, + Filter = DocumentFilter.Create(name, tag), + OutputPath = actualPath + }; + } + + public object WriteDocument(Document document) + { + return WriteDocumentHook?.Invoke(Option, document); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/PSDocsHelper.cs b/packages/psdocs/src/PSDocs/Models/PSDocsHelper.cs new file mode 100644 index 00000000..332a641a --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/PSDocsHelper.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace PSDocs.Models +{ + public static class PSDocsHelper + { + /// + /// Check if the defined tags match the expected tags. + /// + /// The tags of the document definition. + /// The tags to match. + /// Returns true when all the matching tags are found on the document definition. + public static bool MatchTags(string[] definitionTags, string[] match) + { + if (match == null || match.Length == 0) + { + return true; + } + + if (definitionTags == null || definitionTags.Length < match.Length) + { + return false; + } + + var tags = new HashSet(definitionTags, StringComparer.InvariantCultureIgnoreCase); + + foreach (var m in match) + { + if (!tags.Contains(m)) + { + return false; + } + } + + return true; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Section.cs b/packages/psdocs/src/PSDocs/Models/Section.cs new file mode 100644 index 00000000..11d4c7bf --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Section.cs @@ -0,0 +1,10 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public sealed class Section : SectionNode + { + public override DocumentNodeType Type => DocumentNodeType.Section; + } +} diff --git a/packages/psdocs/src/PSDocs/Models/SectionNode.cs b/packages/psdocs/src/PSDocs/Models/SectionNode.cs new file mode 100644 index 00000000..63e85652 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/SectionNode.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Management.Automation; + +namespace PSDocs.Models +{ + public abstract class SectionNode : DocumentNode + { + protected SectionNode() + { + Title = string.Empty; + Node = new List(); + Level = 1; + } + + public string Title { get; set; } + + public int Level { get; set; } + + public List Node { get; set; } + + internal bool AddNodes(Collection collection) + { + if (collection == null || collection.Count == 0) + return false; + + var items = new PSObject[collection.Count]; + collection.CopyTo(items, 0); + + var count = 0; + for (var i = 0; i < items.Length; i++) + { + if (items[i] == null || items[i].BaseObject == null) + continue; + + if (items[i].BaseObject is not DocumentNode node) + node = new Text { Content = items[i].BaseObject.ToString() }; + + count++; + Node.Add(node); + } + return count > 0; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Table.cs b/packages/psdocs/src/PSDocs/Models/Table.cs new file mode 100644 index 00000000..b9e66e19 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Table.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace PSDocs.Models +{ + public sealed class Table : DocumentNode + { + public Table() + { + Headers = new List(); + Rows = new List(); + } + + public override DocumentNodeType Type => DocumentNodeType.Table; + + public List Rows { get; set; } + + public List Headers { get; set; } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/TableBuilder.cs b/packages/psdocs/src/PSDocs/Models/TableBuilder.cs new file mode 100644 index 00000000..fb81fd0b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/TableBuilder.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using PSDocs.Pipeline; +using PSDocs.Resources; + +namespace PSDocs.Models +{ + public sealed class TableBuilder + { + private const string FIELD_NAME = "name"; + private const string FIELD_LABEL = "label"; + private const string FIELD_WIDTH = "width"; + private const string FIELD_ALIGNMENT = "alignment"; + + private readonly List _Headers; + + public TableBuilder() + { + _Headers = new List(); + } + + public Table Build() + { + return new Table + { + Headers = _Headers + }; + } + + public void Header(string label) + { + _Headers.Add(new TableColumnHeader + { + Label = label + }); + } + + public void Header(Hashtable hashtable) + { + var header = new TableColumnHeader(); + + // Build index to allow mapping + var index = GetIndex(hashtable); + + // Start loading matching values + if (index.TryGetValue(FIELD_NAME, out var value)) + header.Label = (string)value; + + if (index.TryGetValue(FIELD_LABEL, out value)) + header.Label = (string)value; + + if (index.TryGetValue(FIELD_WIDTH, out value)) + header.Width = (int)value; + + if (index.TryGetValue(FIELD_ALIGNMENT, out value)) + header.Alignment = (Alignment)Enum.Parse(typeof(Alignment), (string)value, true); + + // Validate header + if (string.IsNullOrEmpty(header.Label)) + throw new RuntimeException(PSDocsResources.LabelNullOrEmpty); + + _Headers.Add(header); + } + + public IDictionary GetIndex(Hashtable hashtable) + { + // Build index to allow mapping + var index = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (DictionaryEntry entry in hashtable) + index.Add(entry.Key.ToString(), entry.Value); + + return index; + } + + public IDictionary GetPropertyFilter(Hashtable hashtable) + { + var index = GetIndex(hashtable); + if (index.ContainsKey(FIELD_ALIGNMENT)) + index.Remove(FIELD_ALIGNMENT); + + if (index.ContainsKey(FIELD_WIDTH)) + index.Remove(FIELD_WIDTH); + + if (index.ContainsKey(FIELD_NAME)) + { + index[FIELD_LABEL] = index[FIELD_NAME]; + index.Remove(FIELD_NAME); + } + return index; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/TableColumnHeader.cs b/packages/psdocs/src/PSDocs/Models/TableColumnHeader.cs new file mode 100644 index 00000000..1c54c8b0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/TableColumnHeader.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public sealed class TableColumnHeader + { + public TableColumnHeader() + { + Alignment = Alignment.Undefined; + } + + public Alignment Alignment { get; set; } + + public string Label { get; set; } + + public int Width { get; set; } + } +} diff --git a/packages/psdocs/src/PSDocs/Models/Text.cs b/packages/psdocs/src/PSDocs/Models/Text.cs new file mode 100644 index 00000000..490f5ca3 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Models/Text.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + public sealed class Text : DocumentNode + { + public override DocumentNodeType Type => DocumentNodeType.Text; + + public string Content { get; set; } + } +} diff --git a/packages/psdocs/src/PSDocs/PSDocs.Format.ps1xml b/packages/psdocs/src/PSDocs/PSDocs.Format.ps1xml new file mode 100644 index 00000000..24d85d6a --- /dev/null +++ b/packages/psdocs/src/PSDocs/PSDocs.Format.ps1xml @@ -0,0 +1,36 @@ + + + + + + PSDocs.Runtime.ScriptDocumentBlock + + PSDocs.Runtime.ScriptDocumentBlock + + + + + + + + + + + + + Name + + + Module + + + + + + + + diff --git a/packages/psdocs/src/PSDocs/PSDocs.csproj b/packages/psdocs/src/PSDocs/PSDocs.csproj new file mode 100644 index 00000000..756bc023 --- /dev/null +++ b/packages/psdocs/src/PSDocs/PSDocs.csproj @@ -0,0 +1,71 @@ + + + + netstandard2.0 + 11.0 + Library + {1f6df554-c081-40d8-9aca-32c1abe4a1b6} + portable + en-US + true + true + + Bernie White + PSDocs + https://github.com/microsoft/PSDocs + https://github.com/microsoft/PSDocs/blob/main/LICENSE + 0.0.1 + Copyright (c) Microsoft Corporation. Licensed under the MIT License. + Generate markdown from objects using PowerShell syntax. + +This project uses GitHub Issues to track bugs and feature requests. See GitHub project for more information. + + + + AllEnabledByDefault + true + + + + Windows + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + True + True + PSDocsResources.resx + + + True + True + ViewStrings.resx + + + + + + ResXFileCodeGenerator + PSDocsResources.Designer.cs + + + PublicResXFileCodeGenerator + ViewStrings.Designer.cs + + + + diff --git a/packages/psdocs/src/PSDocs/PSDocs.psd1 b/packages/psdocs/src/PSDocs/PSDocs.psd1 new file mode 100644 index 00000000..b2285128 --- /dev/null +++ b/packages/psdocs/src/PSDocs/PSDocs.psd1 @@ -0,0 +1,144 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# PSDocs module +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'PSDocs.psm1' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +CompatiblePSEditions = 'Core', 'Desktop' + +# ID used to uniquely identify this module +GUID = '1f6df554-c081-40d8-9aca-32c1abe4a1b6' + +# Author of this module +Author = 'Microsoft Corporation' + +# Company or vendor of this module +CompanyName = 'Microsoft Corporation' + +# Copyright statement for this module +Copyright = '(c) Microsoft Corporation. All rights reserved.' + +# Description of the functionality provided by this module +Description = 'Generate markdown from PowerShell. + +This project uses GitHub Issues to track bugs and feature requests. See GitHub project for more information.' + +# Minimum version of the Windows PowerShell engine required by this module +PowerShellVersion = '5.1' + +# Name of the Windows PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the Windows PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +DotNetFrameworkVersion = '4.7.2' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# CLRVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +RequiredAssemblies = @( + 'PSDocs.dll' +) + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +FormatsToProcess = @( + 'PSDocs.Format.ps1xml' +) + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @( + 'Document' + 'Invoke-PSDocument' + 'Get-PSDocument' + 'Get-PSDocumentHeader' + 'New-PSDocumentOption' + 'Section' + 'Table' + 'Metadata' + 'Title' + 'Code' + 'BlockQuote' + 'Note' + 'Warning' + 'Include' + 'Export-PSDocumentConvention' +) + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = @( + 'PSDocs' +) + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Markdown', 'PSDocs', 'DevOps', 'CI') + + # A URL to the license for this module. + LicenseUri = 'https://github.com/Microsoft/PSDocs/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/Microsoft/PSDocs' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = 'https://github.com/Microsoft/PSDocs/releases' + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} diff --git a/packages/psdocs/src/PSDocs/PSDocs.psm1 b/packages/psdocs/src/PSDocs/PSDocs.psm1 new file mode 100644 index 00000000..85a3c01c --- /dev/null +++ b/packages/psdocs/src/PSDocs/PSDocs.psm1 @@ -0,0 +1,929 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# PSDocs module +# + +Set-StrictMode -Version latest; + +[PSDocs.Configuration.PSDocumentOption]::UseExecutionContext($ExecutionContext); +[PSDocs.Configuration.PSDocumentOption]::UseCurrentCulture(); +$Script:UTF8_NO_BOM = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $False; + +# +# Localization +# + +Import-LocalizedData -BindingVariable LocalizedHelp -FileName 'PSDocs.Resources.psd1' -ErrorAction SilentlyContinue; +if ($Null -eq (Get-Variable -Name LocalizedHelp -ErrorAction SilentlyContinue)) { + Import-LocalizedData -BindingVariable LocalizedHelp -FileName 'PSDocs.Resources.psd1' -UICulture 'en-US' -ErrorAction SilentlyContinue; +} + +# +# Public functions +# + +#region Cmdlets + +# .ExternalHelp PSDocs-Help.xml +function Invoke-PSDocument { + [CmdletBinding(DefaultParameterSetName = 'Input')] + param ( + [Parameter(Mandatory = $True, ParameterSetName = 'InputPath')] + [Alias('f')] + [String[]]$InputPath, + + [Parameter(Mandatory = $False)] + [Alias('m')] + [String[]]$Module, + + # The name of the document + [Parameter(Mandatory = $False)] + [Alias('n')] + [String[]]$Name, + + [Parameter(Mandatory = $False)] + [String[]]$Tag, + + [Parameter(Mandatory = $False)] + [String[]]$InstanceName, + + [Parameter(Mandatory = $False, ValueFromPipeline = $True, ParameterSetName = 'Input')] + [PSObject]$InputObject, + + # The path to look for document definitions in + [Parameter(Position = 0, Mandatory = $False)] + [PSDefaultValue(Help = '.')] + [Alias('p')] + [String[]]$Path = $PWD, + + [Parameter(Mandatory = $False)] + [Alias('InputFormat')] + [ValidateSet('None', 'Yaml', 'Json', 'PowerShellData', 'Detect')] + [PSDocs.Configuration.InputFormat]$Format = [PSDocs.Configuration.InputFormat]::Detect, + + # The output path to save generated documentation + [Parameter(Mandatory = $False)] + [String]$OutputPath = $PWD, + + [Parameter(Mandatory = $False)] + [Switch]$PassThru, + + [Parameter(Mandatory = $False)] + [PSDocs.Configuration.PSDocumentOption]$Option, + + [Parameter(Mandatory = $False)] + [PSDocs.Configuration.MarkdownEncoding]$Encoding = [PSDocs.Configuration.MarkdownEncoding]::Default, + + [Parameter(Mandatory = $False)] + [String[]]$Culture, + + [Parameter(Mandatory = $False)] + [String[]]$Convention + ) + begin { + Write-Verbose -Message "[Invoke-PSDocument]::BEGIN"; + $pipelineReady = $False; + + # Check if the path is a directory + if (!(Test-Path -Path $Path)) { + Write-Error -Message $LocalizedHelp.PathNotFound -ErrorAction Stop; + return; + } + + # Get parameter options, which will override options from other sources + $optionParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Option')) { + $optionParams['Option'] = $Option; + } + + # Get an options object + $Option = New-PSDocumentOption @optionParams; + + # Discover scripts in the specified paths + $sourceParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Path')) { + $sourceParams['Path'] = $Path; + } + if ($PSBoundParameters.ContainsKey('Module')) { + $sourceParams['Module'] = $Module; + } + if ($sourceParams.Count -eq 0) { + $sourceParams['Path'] = $Path; + } + $sourceParams['Option'] = $Option; + [PSDocs.Pipeline.Source[]]$sourceFiles = GetSource @sourceParams -Verbose:$VerbosePreference; + + # Check that some matching script files were found + if ($Null -eq $sourceFiles) { + Write-Warning -Message $LocalizedHelp.SourceNotFound; + return; # continue causes issues with Pester + } + + $isDeviceGuard = IsDeviceGuardEnabled; + + # If DeviceGuard is enabled, force a contrained execution environment + if ($isDeviceGuard) { + $Option.Execution.LanguageMode = [PSDocs.Configuration.LanguageMode]::ConstrainedLanguage; + } + + # Get parameter options, which will override options from other sources + if ($PSBoundParameters.ContainsKey('Name')) { + $Option.Document.Include = $Name; + } + if ($PSBoundParameters.ContainsKey('Tag')) { + $Option.Document.Tag = $Tag; + } + if ($PSBoundParameters.ContainsKey('Format')) { + $Option.Input.Format = $Format; + } + if ($PSBoundParameters.ContainsKey('OutputPath') -and !$PassThru) { + $Option.Output.Path = $OutputPath; + } + if ($PSBoundParameters.ContainsKey('Culture')) { + $Option.Output.Culture = $Culture; + } + if ($PSBoundParameters.ContainsKey('Encoding')) { + $Option.Markdown.Encoding = $Encoding; + } + + $builder = [PSDocs.Pipeline.PipelineBuilder]::Invoke($sourceFiles, $Option, $PSCmdlet, $ExecutionContext); + $builder.InstanceName($InstanceName); + $builder.Convention($Convention); + if ($PSBoundParameters.ContainsKey('InputPath')) { + $builder.InputPath($InputPath); + } + try { + $pipeline = $builder.Build(); + if ($Null -ne $pipeline) { + $pipeline.Begin(); + $pipelineReady = $True; + } + } + catch { + throw $_.Exception.GetBaseException(); + } + } + process { + if ($pipelineReady) { + try { + # Process pipeline objects + $pipeline.Process($InputObject); + } + catch { + $pipeline.Dispose(); + throw; + } + } + } + end { + if ($pipelineReady) { + try { + $pipeline.End(); + } + finally { + $pipeline.Dispose(); + } + } + Write-Verbose -Message "[Invoke-PSDocument]::END"; + } +} + +# .ExternalHelp PSDocs-Help.xml +function Get-PSDocument { + [CmdletBinding()] + [OutputType([PSDocs.Definitions.IDocumentDefinition])] + param ( + [Parameter(Mandatory = $False)] + [Alias('m')] + [String[]]$Module, + + [Parameter(Mandatory = $False)] + [Switch]$ListAvailable, + + # Filter to documents with the following names + [Parameter(Mandatory = $False)] + [Alias('n')] + [String[]]$Name, + + # A list of paths to check for definitions + [Parameter(Mandatory = $False, Position = 0)] + [Alias('p')] + [String[]]$Path = $PWD, + + [Parameter(Mandatory = $False)] + [PSDocs.Configuration.PSDocumentOption]$Option + ) + begin { + Write-Verbose -Message "[Get-PSDocument]::BEGIN"; + $pipelineReady = $False; + + # Get parameter options, which will override options from other sources + $optionParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Option')) { + $optionParams['Option'] = $Option; + } + + # Get an options object + $Option = New-PSDocumentOption @optionParams; + + # Discover scripts in the specified paths + $sourceParams = @{ }; + + if ($PSBoundParameters.ContainsKey('Path')) { + $sourceParams['Path'] = $Path; + } + if ($PSBoundParameters.ContainsKey('Module')) { + $sourceParams['Module'] = $Module; + } + if ($PSBoundParameters.ContainsKey('ListAvailable')) { + $sourceParams['ListAvailable'] = $ListAvailable; + } + if ($sourceParams.Count -eq 0) { + $sourceParams['Path'] = $Path; + } + $sourceParams['Option'] = $Option; + [PSDocs.Pipeline.Source[]]$sourceFiles = GetSource @sourceParams -Verbose:$VerbosePreference; + + # Check that some matching script files were found + if ($Null -eq $sourceFiles) { + Write-Verbose -Message $LocalizedHelp.SourceNotFound; + return; # continue causes issues with Pester + } + + Write-Verbose -Message "[Get-PSDocument] -- Found $($sourceFiles.Length) source file(s)"; + + $isDeviceGuard = IsDeviceGuardEnabled; + + # If DeviceGuard is enabled, force a contrained execution environment + if ($isDeviceGuard) { + $Option.Execution.LanguageMode = [PSDocs.Configuration.LanguageMode]::ConstrainedLanguage; + } + + # Get parameter options, which will override options from other sources + if ($PSBoundParameters.ContainsKey('Name')) { + $Option.Document.Include = $Name; + } + + $builder = [PSDocs.Pipeline.PipelineBuilder]::Get($sourceFiles, $Option, $PSCmdlet, $ExecutionContext); + try { + $pipeline = $builder.Build(); + if ($Null -ne $pipeline) { + $pipeline.Begin(); + $pipelineReady = $True; + } + } + catch { + throw $_.Exception.GetBaseException(); + } + } + end { + if ($pipelineReady) { + try { + $pipeline.End(); + } + finally { + $pipeline.Dispose(); + } + } + Write-Verbose -Message "[Get-PSDocument]::END"; + } +} + +# .ExternalHelp PSDocs-Help.xml +function Get-PSDocumentHeader { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $False, ValueFromPipelineByPropertyName = $True)] + [Alias('FullName')] + [String]$Path = $PWD + ) + process { + $filteredItems = Get-ChildItem -Path (Join-Path -Path $Path -ChildPath '*') -File; + foreach ($item in $filteredItems) { + ReadYamlHeader -Path $item.FullName -Verbose:$VerbosePreference; + } + } +} + +# .ExternalHelp PSDocs-Help.xml +function New-PSDocumentOption { + [CmdletBinding(DefaultParameterSetName = 'FromPath')] + [OutputType([PSDocs.Configuration.PSDocumentOption])] + [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Creates an in memory object only')] + param ( + [Parameter(Position = 0, Mandatory = $False, ParameterSetName = 'FromPath')] + [String]$Path = $PWD, + + [Parameter(Mandatory = $True, ParameterSetName = 'FromOption')] + [PSDocs.Configuration.PSDocumentOption]$Option, + + [Parameter(Mandatory = $True, ParameterSetName = 'FromDefault')] + [Switch]$Default, + + # Options + + # Sets the Input.Format option + [Parameter(Mandatory = $False)] + [Alias('InputFormat')] + [ValidateSet('None', 'Yaml', 'Json', 'PowerShellData', 'Detect')] + [PSDocs.Configuration.InputFormat]$Format = [PSDocs.Configuration.InputFormat]::Detect, + + # Sets the Input.ObjectPath option + [Parameter(Mandatory = $False)] + [String]$InputObjectPath, + + # Sets the Input.PathIgnore option + [Parameter(Mandatory = $False)] + [String[]]$InputPathIgnore, + + # Sets the Markdown.Encoding option + [Parameter(Mandatory = $False)] + [Alias('MarkdownEncoding')] + [PSDocs.Configuration.MarkdownEncoding]$Encoding = [PSDocs.Configuration.MarkdownEncoding]::Default, + + # Sets the Output.Culture option + [Parameter(Mandatory = $False)] + [Alias('OutputCulture')] + [String[]]$Culture, + + # Sets the Output.Path option + [Parameter(Mandatory = $False)] + [String]$OutputPath + ) + begin { + Write-Verbose -Message "[New-PSDocumentOption] BEGIN::"; + + # Get parameter options, which will override options from other sources + $optionParams = @{ }; + $optionParams += $PSBoundParameters; + + # Remove invalid parameters + if ($optionParams.ContainsKey('Path')) { + $optionParams.Remove('Path'); + } + if ($optionParams.ContainsKey('Option')) { + $optionParams.Remove('Option'); + } + if ($optionParams.ContainsKey('Default')) { + $optionParams.Remove('Default'); + } + if ($optionParams.ContainsKey('Verbose')) { + $optionParams.Remove('Verbose'); + } + if ($PSBoundParameters.ContainsKey('Option')) { + $Option = [PSDocs.Configuration.PSDocumentOption]::FromFileOrEmpty($Option, $Path); + } + elseif ($PSBoundParameters.ContainsKey('Path')) { + Write-Verbose -Message "Attempting to read: $Path"; + $Option = [PSDocs.Configuration.PSDocumentOption]::FromFile($Path); + } + elseif ($PSBoundParameters.ContainsKey('Default')) { + $Option = [PSDocs.Configuration.PSDocumentOption]::FromDefault(); + } + else { + Write-Verbose -Message "Attempting to read: $Path"; + $Option = [PSDocs.Configuration.PSDocumentOption]::FromFileOrEmpty($Option, $Path); + } + } + end { + # Options + $Option | SetOptions @optionParams -Verbose:$VerbosePreference; + + Write-Verbose -Message "[New-PSDocumentOption] END::"; + } +} + +#endregion Cmdlets + +# +# Internal language keywords +# + +#region Keywords + +function Export-PSDocumentConvention { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + [Parameter(Mandatory = $False)] + [ScriptBlock]$Begin, + + [Parameter(Position = 1, Mandatory = $True)] + [ScriptBlock]$Process, + + [Parameter(Mandatory = $False)] + [ScriptBlock]$End + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +# Implement the Document keyword +function Document { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + [Parameter(Mandatory = $False)] + [String[]]$Tag, + + [Parameter(Position = 1, Mandatory = $True)] + [ScriptBlock]$Body, + + [Parameter(Mandatory = $False)] + [ScriptBlock]$If, + + [Parameter(Mandatory = $False)] + [String[]]$With + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +# Implement the Section keyword +function Section { + [CmdletBinding()] + [OutputType([PSObject])] + param ( + # The name of the Section + [Parameter(Position = 0, Mandatory = $True)] + [String]$Name, + + # A script block with the body of the Section + [Parameter(Position = 1, Mandatory = $True)] + [ScriptBlock]$Body, + + # Optionally a condition that must be met prior to including the Section + [Parameter(Mandatory = $False)] + [Alias('When')] + [ScriptBlock]$If, + + # Optionally create a section block even when it is empty + [Parameter(Mandatory = $False)] + [Switch]$Force = $False + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Title { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [AllowEmptyString()] + [String]$Content + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Code { + [CmdletBinding()] + [OutputType([PSDocs.Models.Code])] + param ( + # Body of the code block + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'Default', ValueFromPipeline = $True)] + [Parameter(Position = 1, Mandatory = $True, ParameterSetName = 'InfoString', ValueFromPipeline = $True)] + [ScriptBlock]$Body, + + [Parameter(Mandatory = $True, ParameterSetName = 'StringDefault', ValueFromPipeline = $True)] + [Parameter(Mandatory = $True, ParameterSetName = 'StringInfoString', ValueFromPipeline = $True)] + [String]$BodyString, + + # Info-string + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'InfoString')] + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'StringInfoString')] + [String]$Info + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Note { + [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')] + [OutputType([PSDocs.Models.BlockQuote])] + param ( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ScriptBlock')] + [ScriptBlock]$Body, + + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ParameterSetName = 'Text')] + [String]$Text + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Warning { + [CmdletBinding(DefaultParameterSetName = 'ScriptBlock')] + [OutputType([PSDocs.Models.BlockQuote])] + param ( + [Parameter(Position = 0, Mandatory = $True, ParameterSetName = 'ScriptBlock')] + [ScriptBlock]$Body, + + [Parameter(Mandatory = $True, ValueFromPipeline = $True, ParameterSetName = 'Text')] + [String]$Text + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function BlockQuote { + [CmdletBinding()] + [OutputType([PSDocs.Models.BlockQuote])] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [String]$Text, + + [Parameter(Mandatory = $False)] + [String]$Info, + + [Parameter(Mandatory = $False)] + [String]$Title + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Include { + [CmdletBinding()] + [OutputType([PSDocs.Models.Include])] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [String]$FileName, + + [Parameter(Mandatory = $False)] + [String]$BaseDirectory = $PWD, + + [Parameter(Mandatory = $False)] + [String]$Culture = $Culture, + + [Parameter(Mandatory = $False)] + [Switch]$UseCulture = $False, + + [Parameter(Mandatory = $False)] + [System.Collections.IDictionary]$Replace + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Metadata { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Position = 0, Mandatory = $True)] + [AllowNull()] + [System.Collections.IDictionary]$Body + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +function Table { + [CmdletBinding()] + [OutputType([PSDocs.Models.Table])] + param ( + [Parameter(Mandatory = $False, ValueFromPipeline = $True)] + [AllowNull()] + [Object]$InputObject, + + [Parameter(Mandatory = $False, Position = 0)] + [Object[]]$Property + ) + begin { + # This is just a stub to improve authoring and discovery + Write-Error -Message $LocalizedHelp.KeywordOutsideEngine -Category InvalidOperation; + } +} + +#endregion Keywords + +# +# Helper functions +# + +function SetOptions { + [CmdletBinding()] + [OutputType([PSDocs.Configuration.PSDocumentOption])] + param ( + [Parameter(Mandatory = $True, ValueFromPipeline = $True)] + [PSDocs.Configuration.PSDocumentOption]$InputObject, + + # Options + + # Sets the Input.Format option + [Parameter(Mandatory = $False)] + [Alias('InputFormat')] + [ValidateSet('None', 'Yaml', 'Json', 'PowerShellData', 'Detect')] + [PSDocs.Configuration.InputFormat]$Format = [PSDocs.Configuration.InputFormat]::Detect, + + # Sets the Input.ObjectPath option + [Parameter(Mandatory = $False)] + [String]$InputObjectPath, + + # Sets the Input.PathIgnore option + [Parameter(Mandatory = $False)] + [String[]]$InputPathIgnore, + + # Sets the Markdown.Encoding option + [Parameter(Mandatory = $False)] + [ValidateSet('Default', 'UTF8', 'UTF7', 'Unicode', 'UTF32', 'ASCII')] + [PSDocs.Configuration.MarkdownEncoding]$Encoding = 'Default', + + # Sets the Output.Culture option + [Parameter(Mandatory = $False)] + [String[]]$Culture, + + # Sets the Output.Path option + [Parameter(Mandatory = $False)] + [String]$OutputPath + ) + process { + # Options + + # Sets option Input.Format + if ($PSBoundParameters.ContainsKey('Format')) { + $InputObject.Input.Format = $Format; + } + + # Sets option Input.ObjectPath + if ($PSBoundParameters.ContainsKey('InputObjectPath')) { + $InputObject.Input.ObjectPath = $InputObjectPath; + } + + # Sets option Input.Encoding + if ($PSBoundParameters.ContainsKey('InputPathIgnore')) { + $InputObject.Input.PathIgnore = $InputPathIgnore; + } + + # Sets option Markdown.Encoding + if ($PSBoundParameters.ContainsKey('Encoding')) { + $InputObject.Markdown.Encoding = $Encoding; + } + + # Sets option Output.Culture + if ($PSBoundParameters.ContainsKey('Culture')) { + $InputObject.Output.Culture = $Culture; + } + + # Sets option Output.Path + if ($PSBoundParameters.ContainsKey('OutputPath')) { + $InputObject.Output.Path = $OutputPath; + } + + return $InputObject; + } +} + +function InitDocumentContext { + [CmdletBinding()] + param () + process { + + if ($Null -eq (Get-Variable -Name DocumentBody -Scope Script -ErrorAction SilentlyContinue)) { + $Script:DocumentBody = @{ }; + } + } +} + +# Get a list of document script files in the matching paths +function GetSource { + [CmdletBinding()] + [OutputType([PSDocs.Pipeline.Source])] + param ( + [Parameter(Mandatory = $False)] + [String[]]$Path, + + [Parameter(Mandatory = $False)] + [String[]]$Module, + + [Parameter(Mandatory = $False)] + [Switch]$ListAvailable, + + [Parameter(Mandatory = $False)] + [String]$Culture, + + [Parameter(Mandatory = $False)] + [Switch]$PreferPath = $False, + + [Parameter(Mandatory = $False)] + [Switch]$PreferModule = $False, + + [Parameter(Mandatory = $True)] + [PSDocs.Configuration.PSDocumentOption]$Option + ) + process { + $builder = [PSDocs.Pipeline.PipelineBuilder]::Source($Option, $PSCmdlet, $ExecutionContext); + if ($PSBoundParameters.ContainsKey('Path')) { + try { + $builder.Directory($Path); + } + catch { + throw $_.Exception.GetBaseException(); + } + } + + $moduleParams = @{}; + if ($PSBoundParameters.ContainsKey('Module')) { + $moduleParams['Name'] = $Module; + + # Determine if module should be automatically loaded + if (GetAutoloadPreference) { + foreach ($m in $Module) { + if ($Null -eq (GetModule -Name $m)) { + LoadModule -Name $m -Verbose:$VerbosePreference; + } + } + } + } + + if ($PSBoundParameters.ContainsKey('ListAvailable')) { + $moduleParams['ListAvailable'] = $ListAvailable.ToBool(); + } + + if ($moduleParams.Count -gt 0 -or $PreferModule) { + $modules = @(GetModule @moduleParams); + $builder.Module($modules); + } + $builder.Build(); + } +} + +function GetAutoloadPreference { + [CmdletBinding()] + [OutputType([System.Boolean])] + param () + process { + $v = Microsoft.PowerShell.Utility\Get-Variable -Name 'PSModuleAutoLoadingPreference' -ErrorAction SilentlyContinue; + return ($Null -eq $v) -or ($v.Value -eq [System.Management.Automation.PSModuleAutoLoadingPreference]::All); + } +} + +function GetModule { + [CmdletBinding()] + [OutputType([System.Management.Automation.PSModuleInfo])] + param ( + [Parameter(Mandatory = $False)] + [String[]]$Name, + + [Parameter(Mandatory = $False)] + [Switch]$ListAvailable = $False + ) + process { + $moduleResults = (Microsoft.PowerShell.Core\Get-Module @PSBoundParameters | Microsoft.PowerShell.Core\Where-Object -FilterScript { + 'PSDocs-documents' -in $_.Tags + } | Microsoft.PowerShell.Utility\Group-Object -Property Name) + + if ($Null -ne $moduleResults) { + foreach ($m in $moduleResults) { + @($m.Group | Microsoft.PowerShell.Utility\Sort-Object -Descending -Property Version)[0]; + } + } + } +} + +function LoadModule { + [CmdletBinding()] + [OutputType([void])] + param ( + [Parameter(Mandatory = $True)] + [String]$Name + ) + process{ + $Null = GetModule -Name $Name -ListAvailable | Microsoft.PowerShell.Core\Import-Module -Global; + } +} + +function ReadYamlHeader { + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + + process { + # Read the file + $content = Get-Content -Path $Path -Raw; + + # Detect Yaml header + if (![String]::IsNullOrEmpty($content) -and $content -match '^(---(\r|\n|\r\n)(?([A-Z0-9]{1,}:[A-Z0-9 ]{1,}(\r|\n|\r\n){0,}){1,})(\r|\n|\r\n)---(\r|\n|\r\n))') { + Write-Verbose -Message "[Doc][Toc]`t-- Reading Yaml header: $Path"; + + # Extract yaml header key value pair + [String[]]$yamlHeader = $Matches.yaml -split "`n"; + $result = @{ }; + + # Read key values into hashtable + foreach ($item in $yamlHeader) { + $kv = $item.Split(':', 2, [System.StringSplitOptions]::RemoveEmptyEntries); + + Write-Debug -Message "Found yaml keypair from: $item"; + if ($kv.Length -eq 2) { + $result[$kv[0].Trim()] = $kv[1].Trim(); + } + } + + # Emit result to the pipeline + return $result; + } + } +} + +function IsDeviceGuardEnabled { + [CmdletBinding()] + [OutputType([System.Boolean])] + param () + process { + if ((Get-Variable -Name IsMacOS -ErrorAction Ignore) -or (Get-Variable -Name IsLinux -ErrorAction Ignore)) { + return $False; + } + + # PowerShell 6.0.x does not support Device Guard + if ($PSVersionTable.PSVersion -ge '6.0' -and $PSVersionTable.PSVersion -lt '6.1') { + return $False; + } + return [System.Management.Automation.Security.SystemPolicy]::GetSystemLockdownPolicy() -eq [System.Management.Automation.Security.SystemEnforcementMode]::Enforce; + } +} + +function InitEditorServices { + [CmdletBinding()] + param () + process { + Export-ModuleMember -Function @( + 'Section' + 'Table' + 'Metadata' + 'Title' + 'Code' + 'BlockQuote' + 'Note' + 'Warning' + 'Include' + 'Export-PSDocumentConvention' + ); + + # Export variables + Export-ModuleMember -Variable @( + 'PSDocs' + ); + } +} + +# +# Editor services +# + +# Define variables and types +[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignment', '', Justification = 'Variable is used for editor discovery only.')] +[PSDocs.Runtime.PSDocs]$PSDocs = [PSDocs.Runtime.PSDocs]::new(); + +if ($Null -ne (Get-Variable -Name psEditor -ErrorAction Ignore)) { + InitEditorServices; +} + +# +# Export module +# + +Export-ModuleMember -Function @( + 'Document' + 'Invoke-PSDocument' + 'Get-PSDocument' + 'Get-PSDocumentHeader' + 'New-PSDocumentOption' +); + +# EOM diff --git a/packages/psdocs/src/PSDocs/Pipeline/Execeptions.cs b/packages/psdocs/src/PSDocs/Pipeline/Execeptions.cs new file mode 100644 index 00000000..a59aef9c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/Execeptions.cs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Runtime.Serialization; +using System.Security.Permissions; + +namespace PSDocs.Pipeline +{ + /// + /// A base class for all pipeline exceptions. + /// + public abstract class PipelineException : Exception + { + /// + /// Creates a pipeline exception. + /// + protected PipelineException() + { + } + + /// + /// Creates a pipeline exception. + /// + /// The detail of the exception. + protected PipelineException(string message) : base(message) + { + } + + /// + /// Creates a pipeline exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + protected PipelineException(string message, Exception innerException) : base(message, innerException) + { + } + + protected PipelineException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } + + /// + /// A parser exception. + /// + [Serializable] + public sealed class ParseException : PipelineException + { + /// + /// Creates a rule exception. + /// + public ParseException() + { + } + + public ParseException(string message) + : base(message) { } + + public ParseException(string message, Exception innerException) + : base(message, innerException) { } + + /// + /// Creates a rule exception. + /// + /// The detail of the exception. + internal ParseException(string message, string errorId) : base(message) + { + ErrorId = errorId; + } + + /// + /// Creates a rule exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + internal ParseException(string message, string errorId, Exception innerException) : base(message, innerException) + { + ErrorId = errorId; + } + + private ParseException(SerializationInfo info, StreamingContext context) : base(info, context) + { + ErrorId = info.GetString("ErrorId"); + } + + public string ErrorId { get; } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + info.AddValue("ErrorId", ErrorId); + base.GetObjectData(info, context); + } + } + + [Serializable] + public sealed class RuntimeException : PipelineException + { + /// + /// Creates a serialization exception. + /// + public RuntimeException() + { + } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + internal RuntimeException(string message) : base(message) + { + } + + internal RuntimeException(string sourceFile, Exception innerException) : base(innerException.Message, innerException) + { + SourceFile = sourceFile; + } + + private RuntimeException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public string SourceFile { get; } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + base.GetObjectData(info, context); + } + } + + [Serializable] + public sealed class InvokeDocumentException : PipelineException + { + public InvokeDocumentException() + { + + } + + public InvokeDocumentException(string message) + : base(message) + { + + } + + public InvokeDocumentException(string message, Exception innerException) + : base(message, innerException) + { + + } + + public InvokeDocumentException(string message, Exception innerException, string path, string positionMessage) + : base(message, innerException) + { + Path = path; + PositionMessage = positionMessage; + } + + private InvokeDocumentException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + public string Path { get; } + + public string PositionMessage { get; } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) throw new ArgumentNullException(nameof(info)); + base.GetObjectData(info, context); + } + } + + /// + /// An exception when building the pipeline. + /// + [Serializable] + public sealed class PipelineBuilderException : PipelineException + { + /// + /// Creates a pipeline builder exception. + /// + public PipelineBuilderException() + : base() { } + + /// + /// Creates a pipeline builder exception. + /// + /// The detail of the exception. + public PipelineBuilderException(string message) + : base(message) { } + + /// + /// Creates a pipeline builder exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public PipelineBuilderException(string message, Exception innerException) + : base(message, innerException) { } + + private PipelineBuilderException(SerializationInfo info, StreamingContext context) + : base(info, context) { } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); + } + } + + /// + /// A serialization exception. + /// + [Serializable] + public sealed class PipelineSerializationException : PipelineException + { + /// + /// Creates a serialization exception. + /// + public PipelineSerializationException() + { + } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + public PipelineSerializationException(string message) : base(message) + { + } + + /// + /// Creates a serialization exception. + /// + /// The detail of the exception. + /// A nested exception that caused the issue. + public PipelineSerializationException(string message, Exception innerException) : base(message, innerException) + { + } + + private PipelineSerializationException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + + [SecurityPermission(SecurityAction.Demand, SerializationFormatter = true)] + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + if (info == null) + throw new ArgumentNullException(nameof(info)); + + base.GetObjectData(info, context); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/GetPipeline.cs b/packages/psdocs/src/PSDocs/Pipeline/GetPipeline.cs new file mode 100644 index 00000000..cd13600c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/GetPipeline.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Runtime; + +namespace PSDocs.Pipeline +{ + public interface IGetPipelineBuilder : IPipelineBuilder + { + + } + + /// + /// A helper to construct a get pipeline. + /// + internal sealed class GetPipelineBuilder : PipelineBuilderBase, IGetPipelineBuilder + { + internal GetPipelineBuilder(Source[] source, HostContext hostContext) + : base(source, hostContext) { } + + public override IPipeline Build() + { + if (RequireSources()) + return null; + + return new GetPipeline(PrepareContext(), Source); + } + } + + internal sealed class GetPipeline : PipelineBase, IPipeline + { + private readonly RunspaceContext _Runspace; + + internal GetPipeline(PipelineContext context, Source[] source) + : base(context, source) + { + _Runspace = new RunspaceContext(Context); + HostHelper.ImportResource(Source, _Runspace); + } + + public override void End() + { + Context.Writer.WriteObject(HostHelper.GetDocumentBlock(_Runspace, Source), true); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/HostContext.cs b/packages/psdocs/src/PSDocs/Pipeline/HostContext.cs new file mode 100644 index 00000000..812f35d7 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/HostContext.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Pipeline +{ + public interface IHostContext + { + ActionPreference GetPreferenceVariable(string variableName); + + T GetVariable(string variableName); + } + + internal static class HostContextExtensions + { + private const string ErrorPreference = "ErrorActionPreference"; + private const string WarningPreference = "WarningPreference"; + private const string InformationPreference = "InformationPreference"; + private const string VerbosePreference = "VerbosePreference"; + private const string DebugPreference = "DebugPreference"; + private const string AutoLoadingPreference = "PSModuleAutoLoadingPreference"; + + public static ActionPreference GetErrorPreference(this IHostContext hostContext) + { + return hostContext.GetPreferenceVariable(ErrorPreference); + } + + public static ActionPreference GetWarningPreference(this IHostContext hostContext) + { + return hostContext.GetPreferenceVariable(WarningPreference); + } + + public static ActionPreference GetInformationPreference(this IHostContext hostContext) + { + return hostContext.GetPreferenceVariable(InformationPreference); + } + + public static ActionPreference GetVerbosePreference(this IHostContext hostContext) + { + return hostContext.GetPreferenceVariable(VerbosePreference); + } + + public static ActionPreference GetDebugPreference(this IHostContext hostContext) + { + return hostContext.GetPreferenceVariable(DebugPreference); + } + + public static PSModuleAutoLoadingPreference GetAutoLoadingPreference(this IHostContext hostContext) + { + return hostContext.GetVariable(AutoLoadingPreference); + } + } + + internal sealed class HostContext : IHostContext + { + internal readonly PSCmdlet CommandRuntime; + internal readonly EngineIntrinsics ExecutionContext; + + internal HostContext(PSCmdlet commandRuntime, EngineIntrinsics executionContext) + { + CommandRuntime = commandRuntime; + ExecutionContext = executionContext; + } + + public ActionPreference GetPreferenceVariable(string variableName) + { + if (ExecutionContext == null) + return ActionPreference.SilentlyContinue; + + return (ActionPreference)ExecutionContext.SessionState.PSVariable.GetValue(variableName); + } + + public T GetVariable(string variableName) + { + if (ExecutionContext == null) + return default; + + return (T)ExecutionContext.SessionState.PSVariable.GetValue(variableName); + } + + public bool ShouldProcess(string target, string action) + { + if (CommandRuntime == null) + return true; + + return CommandRuntime.ShouldProcess(target, action); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/InputPathBuilder.cs b/packages/psdocs/src/PSDocs/Pipeline/InputPathBuilder.cs new file mode 100644 index 00000000..83779d11 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/InputPathBuilder.cs @@ -0,0 +1,11 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Pipeline +{ + internal sealed class InputPathBuilder : PathBuilder + { + public InputPathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter) + : base(logger, basePath, searchPattern, filter) { } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/InstanceNameBinder.cs b/packages/psdocs/src/PSDocs/Pipeline/InstanceNameBinder.cs new file mode 100644 index 00000000..6f4489aa --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/InstanceNameBinder.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; + +namespace PSDocs.Pipeline +{ + /// + /// Handles binding of instance names to objects. + /// + internal sealed class InstanceNameBinder + { + private readonly string[] _InstanceName; + + internal InstanceNameBinder(string[] instanceName) + { + _InstanceName = instanceName; + } + + internal IEnumerable GetInstanceName(string defaultInstanceName) + { + if (_InstanceName == null || _InstanceName.Length == 0) + yield return defaultInstanceName; + + for (var i = 0; _InstanceName != null && i < _InstanceName.Length; i++) + yield return _InstanceName[i]; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/InvokePipeline.cs b/packages/psdocs/src/PSDocs/Pipeline/InvokePipeline.cs new file mode 100644 index 00000000..126c4ac7 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/InvokePipeline.cs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using PSDocs.Configuration; +using PSDocs.Data; +using PSDocs.Models; +using PSDocs.Processor; +using PSDocs.Processor.Markdown; +using PSDocs.Runtime; + +namespace PSDocs.Pipeline +{ + public interface IInvokePipelineBuilder : IPipelineBuilder + { + void InstanceName(string[] instanceName); + + void Convention(string[] convention); + } + + /// + /// The pipeline builder for Invoke-PSDocument. + /// + internal sealed class InvokePipelineBuilder : PipelineBuilderBase, IInvokePipelineBuilder + { + private string[] _InstanceName; + private string[] _Convention; + private InputFileInfo[] _InputPath; + + + internal InvokePipelineBuilder(Source[] source, HostContext hostContext) + : base(source, hostContext) + { + _InputPath = null; + } + + public void InputPath(string[] path) + { + if (path == null || path.Length == 0) + return; + + var basePath = PSDocumentOption.GetWorkingPath(); + var filter = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore); + filter.UseGitIgnore(); + + var builder = new InputPathBuilder(Writer, basePath, "*", filter.Build()); + builder.Add(path); + _InputPath = builder.Build(); + } + + public void InstanceName(string[] instanceName) + { + if (instanceName == null || instanceName.Length == 0) + return; + + _InstanceName = instanceName; + } + + public void Convention(string[] convention) + { + if (convention == null || convention.Length == 0) + return; + + _Convention = convention; + } + + public override IPipeline Build() + { + if (RequireSources() || RequireCulture()) + return null; + + return new InvokePipeline(PrepareContext(), Source); + } + + protected override PipelineContext PrepareContext() + { + var instanceNameBinder = new InstanceNameBinder(_InstanceName); + var context = new PipelineContext(GetOptionContext(), PrepareStream(), Writer, OutputVisitor, instanceNameBinder, _Convention); + return context; + } + + protected override PipelineStream PrepareStream() + { + if (!string.IsNullOrEmpty(Option.Input.ObjectPath)) + { + AddVisitTargetObjectAction((targetObject, next) => + { + return PipelineReceiverActions.ReadObjectPath(targetObject, next, Option.Input.ObjectPath, true); + }); + } + + if (Option.Input.Format == InputFormat.Yaml) + { + AddVisitTargetObjectAction((targetObject, next) => + { + return PipelineReceiverActions.ConvertFromYaml(targetObject, next); + }); + } + else if (Option.Input.Format == InputFormat.Json) + { + AddVisitTargetObjectAction((targetObject, next) => + { + return PipelineReceiverActions.ConvertFromJson(targetObject, next); + }); + } + else if (Option.Input.Format == InputFormat.PowerShellData) + { + AddVisitTargetObjectAction((targetObject, next) => + { + return PipelineReceiverActions.ConvertFromPowerShellData(targetObject, next); + }); + } + else if (Option.Input.Format == InputFormat.Detect && _InputPath != null) + { + AddVisitTargetObjectAction((targetObject, next) => + { + return PipelineReceiverActions.DetectInputFormat(targetObject, next); + }); + } + return new PipelineStream(VisitTargetObject, _InputPath); + } + } + + /// + /// The pipeline for Invoke-PSDocument. + /// + internal sealed class InvokePipeline : StreamPipeline, IPipeline + { + private readonly List _Completed; + + private IDocumentBuilder[] _Builder; + private MarkdownProcessor _Processor; + private RunspaceContext _Runspace; + + internal InvokePipeline(PipelineContext context, Source[] source) + : base(context, source) + { + _Runspace = new RunspaceContext(Context); + HostHelper.ImportResource(Source, _Runspace); + _Builder = HostHelper.GetDocumentBuilder(_Runspace, Source); + _Processor = new MarkdownProcessor(); + _Completed = new List(); + } + + protected override void ProcessObject(TargetObject targetObject) + { + try + { + var doc = BuildDocument(targetObject); + for (var i = 0; doc != null && i < doc.Length; i++) + { + var result = WriteDocument(doc[i]); + if (result != null) + { + Context.WriteOutput(result); + _Completed.Add(result); + } + } + } + finally + { + _Runspace.ExitTargetObject(); + } + } + + public override void End() + { + if (_Completed.Count == 0) + return; + + var completed = _Completed.ToArray(); + _Runspace.SetOutput(completed); + try + { + for (var i = 0; i < _Builder.Length; i++) + { + _Builder[i].End(_Runspace, completed); + } + } + finally + { + _Runspace.ClearOutput(); + } + } + + private IDocumentResult WriteDocument(Document document) + { + return _Processor.Process(Context.Option, document); + } + + internal Document[] BuildDocument(TargetObject targetObject) + { + _Runspace.EnterTargetObject(targetObject); + var result = new List(); + for (var c = 0; c < Context.Option.Output.Culture.Length; c++) + { + _Runspace.EnterCulture(Context.Option.Output.Culture[c]); + for (var i = 0; i < _Builder.Length; i++) + { + foreach (var instanceName in Context.InstanceNameBinder.GetInstanceName(_Builder[i].Name)) + { + _Runspace.EnterDocument(instanceName); + try + { + var document = _Builder[i].Process(_Runspace, targetObject.Value); + if (document != null) + result.Add(document); + } + finally + { + _Runspace.ExitDocument(); + } + } + } + } + return result.ToArray(); + } + + #region IDisposable + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _Processor = null; + if (_Builder != null) + { + for (var i = 0; i < _Builder.Length; i++) + _Builder[i].Dispose(); + + _Builder = null; + } + _Runspace.Dispose(); + _Runspace = null; + } + base.Dispose(disposing); + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/OptionContext.cs b/packages/psdocs/src/PSDocs/Pipeline/OptionContext.cs new file mode 100644 index 00000000..dca685ce --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/OptionContext.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using PSDocs.Configuration; + +namespace PSDocs.Pipeline +{ + internal sealed class OptionContext : IPSDocumentOption + { + private readonly IPSDocumentOption _Workspace; + private readonly Dictionary _ModuleScope; + + private IPSDocumentOption _Option; + + public OptionContext(IPSDocumentOption option) + { + _Workspace = option; + _ModuleScope = new Dictionary(); + _Option = _Workspace; + } + + #region IPSDocumentOption + + public ConfigurationOption Configuration => _Option.Configuration; + + public DocumentOption Document => _Option.Document; + + public ExecutionOption Execution => _Option.Execution; + + public InputOption Input => _Option.Input; + + public MarkdownOption Markdown => _Option.Markdown; + + public OutputOption Output => _Option.Output; + + #endregion IPSDocumentOption + + internal enum ScopeType + { + Parameter = 0, + + Explicit = 1, + + Workspace = 2, + + Module = 3 + } + + internal void WithScope(IPSDocumentOption option, ScopeType type, string moduleName = null) + { + if (type == ScopeType.Module && !_ModuleScope.ContainsKey(moduleName)) + { + _ModuleScope[moduleName] = option; + } + } + + internal void SwitchScope(string module) + { + _Option = !string.IsNullOrEmpty(module) && _ModuleScope.TryGetValue(module, out var moduleOption) ? moduleOption : _Workspace; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/Output/HostPipelineWriter.cs b/packages/psdocs/src/PSDocs/Pipeline/Output/HostPipelineWriter.cs new file mode 100644 index 00000000..44a1e0cc --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/Output/HostPipelineWriter.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Pipeline.Output +{ + internal sealed class HostPipelineWriter : PipelineWriterBase, IPipelineWriter + { + private readonly HostContext _HostContext; + + // Track whether Dispose has been called. + private bool _Disposed; + + internal HostPipelineWriter(HostContext hostContext) + : base(hostContext) + { + _HostContext = hostContext; + } + + protected override void DoWriteError(ErrorRecord record) + { + if (GuardOutputReady()) + return; + + _HostContext.CommandRuntime.WriteError(record); + } + + protected override void DoWriteWarning(string text) + { + if (GuardOutputReady()) + return; + + _HostContext.CommandRuntime.WriteWarning(text); + } + + protected override void DoWriteInformation(InformationRecord record) + { + if (GuardOutputReady() || record == null) + return; + + _HostContext.CommandRuntime.WriteInformation(record); + } + + protected override void DoWriteVerbose(string text) + { + if (GuardOutputReady()) + return; + + _HostContext.CommandRuntime.WriteVerbose(text); + } + + protected override void DoWriteDebug(DebugRecord record) + { + if (GuardOutputReady()) + return; + + _HostContext.CommandRuntime.WriteDebug(record.Message); + } + + protected override void DoWriteObject(object sendToPipeline, bool enumerateCollection) + { + if (GuardOutputReady()) + return; + + _HostContext.CommandRuntime.WriteObject(sendToPipeline, enumerateCollection); + } + + private bool GuardOutputReady() + { + return _HostContext == null || _HostContext.CommandRuntime == null; + } + + #region IDisposable + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + // Do nothing + } + _Disposed = true; + } + } + + public void Dispose() + { + Dispose(disposing: true); + System.GC.SuppressFinalize(this); + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PathBuilder.cs b/packages/psdocs/src/PSDocs/Pipeline/PathBuilder.cs new file mode 100644 index 00000000..af8fe46b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PathBuilder.cs @@ -0,0 +1,698 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Data; + +namespace PSDocs.Pipeline +{ + public interface IPathBuilder + { + void Add(string path); + + void Add(System.IO.FileInfo[] fileInfo); + + void Add(PathInfo[] pathInfo); + + InputFileInfo[] Build(); + } + + internal abstract class PathBuilder + { + // Path separators + private const char Slash = '/'; + private const char BackSlash = '\\'; + + private const char Dot = '.'; + private const string CurrentPath = "."; + private const string RecursiveSearchOperator = "**"; + + private static readonly char[] PathLiteralStopCharacters = new char[] { '*', '[', '?' }; + private static readonly char[] PathSeparatorCharacters = new char[] { '\\', '/' }; + + private readonly IPipelineWriter _Logger; + private readonly List _Files; + private readonly HashSet _Paths; + private readonly string _BasePath; + private readonly string _DefaultSearchPattern; + private readonly PathFilter _GlobalFilter; + + protected PathBuilder(IPipelineWriter logger, string basePath, string searchPattern, PathFilter filter) + { + _Logger = logger; + _Files = new List(); + _Paths = new HashSet(); + _BasePath = NormalizePath(PSDocumentOption.GetRootedBasePath(basePath)); + _DefaultSearchPattern = searchPattern; + _GlobalFilter = filter; + } + + public void Add(string[] path) + { + if (path == null || path.Length == 0) + return; + + for (var i = 0; i < path.Length; i++) + Add(path[i]); + } + + public void Add(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + FindFiles(path); + } + + public InputFileInfo[] Build() + { + try + { + return _Files.ToArray(); + } + finally + { + _Files.Clear(); + _Paths.Clear(); + } + } + + private void FindFiles(string path) + { + if (TryUrl(path) || TryPath(path, out path)) + return; + + var pathLiteral = GetSearchParameters(path, out var searchPattern, out var searchOption, out var filter); + var files = Directory.EnumerateFiles(pathLiteral, searchPattern, searchOption); + foreach (var file in files) + if (ShouldInclude(file, filter)) + AddFile(file); + } + + private bool TryUrl(string path) + { + if (!path.IsUri()) + return false; + + AddFile(path); + return true; + } + + private bool TryPath(string path, out string normalPath) + { + normalPath = path; + if (path.IndexOfAny(PathLiteralStopCharacters) > -1) + return false; + + var rootedPath = GetRootedPath(path); + if (Directory.Exists(rootedPath) || path == CurrentPath) + { + if (IsBasePath(rootedPath)) + normalPath = CurrentPath; + + return false; + } + if (!File.Exists(rootedPath)) + { + ErrorNotFound(path); + return false; + } + AddFile(rootedPath); + return true; + } + + private bool IsBasePath(string path) + { + path = IsSeparator(path[path.Length - 1]) ? path : string.Concat(path, Path.DirectorySeparatorChar); + return NormalizePath(path) == _BasePath; + } + + private void ErrorNotFound(string path) + { + if (_Logger == null) + return; + + _Logger.WriteError(new ErrorRecord(new FileNotFoundException(), "PSDocs.PathBuilder.ErrorNotFound", ErrorCategory.ObjectNotFound, path)); + } + + private void AddFile(string path) + { + if (_Paths.Contains(path)) + return; + + _Files.Add(new InputFileInfo(_BasePath, path)); + _Paths.Add(path); + } + + private string GetRootedPath(string path) + { + return Path.IsPathRooted(path) ? path : Path.GetFullPath(Path.Combine(_BasePath, path)); + } + + /// + /// Split a search path into components based on wildcards. + /// + private string GetSearchParameters(string path, out string searchPattern, out SearchOption searchOption, out PathFilter filter) + { + searchOption = SearchOption.AllDirectories; + var pathLiteral = TrimPath(path, out var relativeAnchor); + + if (TryFilter(pathLiteral, out searchPattern, out filter)) + return _BasePath; + + pathLiteral = SplitSearchPath(pathLiteral, out searchPattern); + if ((relativeAnchor || !string.IsNullOrEmpty(pathLiteral)) && !string.IsNullOrEmpty(searchPattern)) + searchOption = SearchOption.TopDirectoryOnly; + + if (string.IsNullOrEmpty(searchPattern)) + searchPattern = _DefaultSearchPattern; + + return GetRootedPath(pathLiteral); + } + + private static string SplitSearchPath(string path, out string searchPattern) + { + // Find the index of the first expression character i.e. out/modules/**/file + var stopIndex = path.IndexOfAny(PathLiteralStopCharacters); + + // Track back to the separator before any expression characters + var literalSeparator = stopIndex > -1 ? path.LastIndexOfAny(PathSeparatorCharacters, stopIndex) + 1 : path.LastIndexOfAny(PathSeparatorCharacters) + 1; + searchPattern = path.Substring(literalSeparator, path.Length - literalSeparator); + return path.Substring(0, literalSeparator); + } + + private bool TryFilter(string path, out string searchPattern, out PathFilter filter) + { + searchPattern = null; + filter = null; + if (UseSimpleSearch(path)) + return false; + + filter = PathFilter.Create(_BasePath, path); + var patternSeparator = path.LastIndexOfAny(PathSeparatorCharacters) + 1; + searchPattern = path.Substring(patternSeparator, path.Length - patternSeparator); + return true; + } + + /// + /// Remove leading ./ or .\ characters indicating a relative path anchor. + /// + /// The path to trim. + /// Returns true when a relative path anchor was present. + /// Return a clean path without the relative path anchor. + private static string TrimPath(string path, out bool relativeAnchor) + { + relativeAnchor = false; + if (path.Length >= 2 && path[0] == Dot && IsSeparator(path[1])) + { + relativeAnchor = true; + return path.Remove(0, 2); + } + return path; + } + + private bool ShouldInclude(string file, PathFilter filter) + { + return (filter == null || filter.Match(file)) && + (_GlobalFilter == null || _GlobalFilter.Match(file)); + } + + [DebuggerStepThrough] + private static bool IsSeparator(char c) + { + return c == Slash || c == BackSlash; + } + + /// + /// Determines if a simple search can be used. + /// + [DebuggerStepThrough] + private static bool UseSimpleSearch(string s) + { + return s.IndexOf(RecursiveSearchOperator, System.StringComparison.OrdinalIgnoreCase) == -1; + } + + [DebuggerStepThrough] + private static string NormalizePath(string path) + { + return path.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); + } + } + + internal sealed class PathFilterBuilder + { + private const string GitIgnoreFileName = ".gitignore"; + + private readonly string _BasePath; + private readonly List _Expressions; + private readonly bool _MatchResult; + + private PathFilterBuilder(string basePath, string[] expressions, bool matchResult) + { + _BasePath = basePath; + _Expressions = expressions == null || expressions.Length == 0 ? new List() : new List(expressions); + _MatchResult = matchResult; + } + + internal static PathFilterBuilder Create(string basePath, string[] expressions, bool matchResult = false) + { + return new PathFilterBuilder(basePath, expressions, matchResult); + } + + internal void UseGitIgnore(string basePath = null) + { + _Expressions.Add(".git/"); + _Expressions.Add("!.git/HEAD"); + ReadFile(Path.Combine(basePath ?? _BasePath, GitIgnoreFileName)); + } + + internal PathFilter Build() + { + return PathFilter.Create(_BasePath, _Expressions.ToArray(), _MatchResult); + } + + private void ReadFile(string filePath) + { + if (File.Exists(filePath)) + _Expressions.AddRange(File.ReadAllLines(filePath)); + } + } + + /// + /// Filters paths based on predefined rules. + /// + internal sealed class PathFilter + { + // Path separators + private const char Slash = '/'; + private const char BackSlash = '\\'; + + // Operators + private const char Asterix = '*'; // Match multiple characters except '/' + private const char Question = '?'; // Match any character except '/' + private const char Hash = '#'; // Comment + private const char Exclamation = '!'; // Include a previously excluded path + + private readonly string _BasePath; + private readonly PathFilterExpression[] _Expression; + private readonly bool _MatchResult; + + private PathFilter(string basePath, PathFilterExpression[] expression, bool matchResult) + { + _BasePath = NormalDirectoryPath(basePath); + _Expression = expression ?? Array.Empty(); + _MatchResult = matchResult; + } + + #region PathStream + + [DebuggerDisplay("Path = '{_Path}', Position = {_Position}, Current = '{_Current}'")] + private sealed class PathStream + { + private readonly string _Path; + private int _Position; + private char _Current; + + public PathStream(string path) + { + _Path = path; + Reset(); + } + + /// + /// Resets the cursor to the start of the path stream. + /// + public void Reset() + { + _Position = -1; + _Current = char.MinValue; + if (_Path[0] == Exclamation) + Next(); + } + + /// + /// Move to the next character. + /// + public bool Next() + { + _Position++; + if (_Position >= _Path.Length) + { + _Current = char.MinValue; + return false; + } + _Current = _Path[_Position]; + return true; + } + + public bool TryMatch(PathStream other, int offset) + { + return other.Peak(offset, out var c) && IsMatch(c); + } + + public bool IsUnmatchedSingle(PathStream other, int offset) + { + return other.Peak(offset, out var c) && IsWilcardQ(c) && other.Peak(offset + 1, out var cnext) && IsMatch(cnext); + } + + private bool IsMatch(char c) + { + return _Current == c || + (IsSeparator(_Current) && IsSeparator(c)) || + (!IsSeparator(_Current) && IsWilcardQ(c)); + } + + /// + /// Determine if the current character sequence is ** or **/. + /// + public bool IsAnyMatchEnding(int offset = 0) + { + if (!IsWildcardAA(offset)) + return false; + + var pos = _Position + offset; + + // Ends in ** + if (pos + 1 == _Path.Length - 1) + return true; + + // Ends in **/ + if (pos + 2 == _Path.Length - 1 && IsSeparator(_Path[pos + 2])) + return true; + + return false; + } + + public bool SkipMatchAA() + { + if (!IsWildcardAA()) + return false; + + + Skip(2); // Skip ** + SkipSeparator(); // Skip **/ + SkipMatchAA(); // Skip **/**/ + return true; + } + + public bool SkipMatchA() + { + if (!IsWildardA()) + return false; + + Skip(1); + return true; + } + + private bool Skip(int count) + { + if (count > 1) + _Position += count - 1; + + return Next(); + } + + [DebuggerStepThrough] + private void SkipSeparator() + { + if (IsSeparator(_Current)) + Next(); + } + + /// + /// Determine if the current character sequence is **. + /// + [DebuggerStepThrough] + private bool IsWildcardAA(int offset = 0) + { + var pos = _Position + offset; + return pos + 1 < _Path.Length && _Path[pos] == Asterix && _Path[pos + 1] == Asterix; + } + + [DebuggerStepThrough] + private bool IsWildardA(int offset = 0) + { + var pos = _Position + offset; + return pos < _Path.Length && _Path[pos] == Asterix; + } + + [DebuggerStepThrough] + private static bool IsWilcardQ(char c) + { + return c == Question; + } + + [DebuggerStepThrough] + private static bool IsSeparator(char c) + { + return c == Slash || c == BackSlash; + } + + /// + /// Match ** + /// + public bool TryMatchAA(PathStream other, int start) + { + var offset = start; + do + { + if (IsUnmatchedSingle(other, offset)) + offset++; + + // Determine if fully matched to end or the next any match + if (other.IsWildcardAA(offset)) + { + other.Skip(offset); + return true; + } + else if (other.IsWildardA(offset) && TryMatchA(other, offset + 1)) + { + return true; + } + + //(IsSingleWildcard(c) && other.Peak(offset + 1, out char cnext) && IsMatch(cnext)) + //if (TryMatchCharacter(other, offset)) + //{ + + //} + // Try to match the remaining + if (TryMatch(other, offset)) + { + offset++; + } + else + { + offset = start; + } + + if (offset + other._Position >= other._Path.Length) + { + other.Skip(offset); + return true; + } + } while (Next()); + return false; + } + + /// + /// Match * + /// + public bool TryMatchA(PathStream other, int start) + { + var offset = start; + do + { + // Determine if fully matched to end or the next any match + if (other.IsWildcardAA(offset)) + { + other.Skip(offset); + return true; + } + //if (other.IsCharacterMatch(offset)) + + // Try to match the remaining + if (TryMatch(other, offset)) + { + offset++; + } + else + { + offset = start; + } + + if (offset + other._Position >= other._Path.Length) + { + Next(); + other.Skip(offset); + return true; + } + } while (Next()); + return false; + } + + private bool Peak(int offset, out char c) + { + if (offset + _Position >= _Path.Length) + { + c = char.MinValue; + return false; + } + c = _Path[offset + _Position]; + return true; + } + } + + #endregion PathStream + + #region PathFilterExpression + + [DebuggerDisplay("{_Expression}")] + private sealed class PathFilterExpression + { + private readonly PathStream _Expression; + private readonly bool _Include; + + private PathFilterExpression(string expression, bool include) + { + _Expression = new PathStream(expression); + _Include = include; + } + + public static PathFilterExpression Create(string expression) + { + if (string.IsNullOrEmpty(expression)) + throw new ArgumentNullException(nameof(expression)); + + var actualInclude = true; + if (expression[0] == Exclamation) + actualInclude = false; + + return new PathFilterExpression(expression, actualInclude); + } + + /// + /// Determine if the path matches the expression. + /// + public void Match(string path, ref bool include) + { + // Only process if the result would change + if (_Include == include || !Match(path)) + return; + + include = !include; + } + + /// + /// Determines if the path matches the expression. + /// + private bool Match(string path) + { + _Expression.Reset(); + var stream = new PathStream(path); + while (stream.Next() && _Expression.Next()) + { + // Match characters + if (stream.TryMatch(_Expression, 0)) + continue; + + // Skip ? when zero characters are being matched + else if (stream.IsUnmatchedSingle(_Expression, 0)) + _Expression.Next(); + + // Match ending wildcards e.g. src/** or src/**/ + else if (_Expression.IsAnyMatchEnding()) + break; + + // Match ending with depth e.g. src/**/bin/ + else if (_Expression.SkipMatchAA()) + { + if (!stream.TryMatchAA(_Expression, 0)) + return false; + } + + // Match wildcard * + else if (_Expression.SkipMatchA()) + { + if (!stream.TryMatchA(_Expression, 0)) + return false; + } + + else return false; + } + return true; + } + } + + #endregion PathFilterExpression + + #region Public methods + + public static PathFilter Create(string basePath, string expression, bool matchResult = true) + { + if (!ShouldSkipExpression(expression)) + return new PathFilter(basePath, new PathFilterExpression[] { PathFilterExpression.Create(expression) }, matchResult); + + return new PathFilter(basePath, null, matchResult); + } + + public static PathFilter Create(string basePath, string[] expression, bool matchResult = true) + { + var result = new List(expression.Length); + for (var i = 0; i < expression.Length; i++) + if (!ShouldSkipExpression(expression[i])) + result.Add(PathFilterExpression.Create(expression[i])); + + return result.Count == 0 ? new PathFilter(basePath, null, matchResult) : new PathFilter(basePath, result.ToArray(), matchResult); + } + + /// + /// Determine if the specific path is matched. + /// + /// The path to evaluate. + public bool Match(string path) + { + var start = 0; + + // Check if path is within base path + if (Path.IsPathRooted(path)) + { + if (!path.StartsWith(_BasePath)) + return !_MatchResult; + + start = _BasePath.Length; + } + var cleanPath = start > 0 ? path.Remove(0, start) : path; + + // Include unless excluded + var result = false; + + // Compare expressions + for (var i = 0; i < _Expression.Length; i++) + _Expression[i].Match(cleanPath, ref result); + + // Flip the result if a match should return false + return _MatchResult ? result : !result; + } + + #endregion Public methods + + private static bool ShouldSkipExpression(string expression) + { + return string.IsNullOrEmpty(expression) || expression[0] == Hash; + } + + private static string NormalDirectoryPath(string path) + { + var c = path[path.Length - 1]; + if (c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar) + return path; + + return string.Concat(path, Path.DirectorySeparatorChar); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PipelineBuilder.cs b/packages/psdocs/src/PSDocs/Pipeline/PipelineBuilder.cs new file mode 100644 index 00000000..716873e7 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PipelineBuilder.cs @@ -0,0 +1,272 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Management.Automation; +using System.Text; +using PSDocs.Configuration; +using PSDocs.Pipeline.Output; +using PSDocs.Processor; +using PSDocs.Resources; + +namespace PSDocs.Pipeline +{ + internal delegate bool ShouldProcess(string target, string action); + + public static class PipelineBuilder + { + /// + /// Invoke-PSDocument. + /// + public static IInvokePipelineBuilder Invoke(Source[] source, IPSDocumentOption option, PSCmdlet commandRuntime, EngineIntrinsics executionContext) + { + var hostContext = new HostContext(commandRuntime, executionContext); + var builder = new InvokePipelineBuilder(source, hostContext); + builder.Configure(option); + return builder; + } + + public static IGetPipelineBuilder Get(Source[] source, IPSDocumentOption option, PSCmdlet commandRuntime, EngineIntrinsics executionContext) + { + var hostContext = new HostContext(commandRuntime, executionContext); + var builder = new GetPipelineBuilder(source, hostContext); + builder.Configure(option); + return builder; + } + + public static SourcePipelineBuilder Source(IPSDocumentOption option, PSCmdlet commandRuntime, EngineIntrinsics executionContext) + { + var hostContext = new HostContext(commandRuntime, executionContext); + var builder = new SourcePipelineBuilder(hostContext); + //builder.Configure(option); + return builder; + } + } + + public interface IPipelineBuilder + { + IPipelineBuilder Configure(IPSDocumentOption option); + + IPipeline Build(); + } + + /// + /// Objects that follow the pipeline lifecycle. + /// + public interface IPipelineDisposable : IDisposable + { + void Begin(); + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] + void End(); + } + + public interface IPipeline : IPipelineDisposable + { + void Process(PSObject sourceObject); + } + + internal abstract class PipelineBuilderBase : IPipelineBuilder + { + protected readonly Source[] Source; + protected readonly PSDocumentOption Option; + protected readonly IPipelineWriter Writer; + protected readonly ShouldProcess ShouldProcess; + + protected Action OutputVisitor; + protected VisitTargetObject VisitTargetObject; + + private static readonly ShouldProcess EmptyShouldProcess = (target, action) => true; + + internal PipelineBuilderBase(Source[] source, HostContext hostContext) + { + Option = new PSDocumentOption(); + Source = source; + Writer = new HostPipelineWriter(hostContext); + ShouldProcess = hostContext == null ? EmptyShouldProcess : hostContext.ShouldProcess; + OutputVisitor = (o, enumerate) => WriteToString(o, enumerate, Writer); + VisitTargetObject = PipelineReceiverActions.PassThru; + } + + public virtual IPipelineBuilder Configure(IPSDocumentOption option) + { + Option.Configuration = new ConfigurationOption(option.Configuration); + Option.Document = DocumentOption.Combine(option.Document, DocumentOption.Default); + Option.Execution = ExecutionOption.Combine(option.Execution, ExecutionOption.Default); + Option.Input = InputOption.Combine(option.Input, InputOption.Default); + Option.Markdown = MarkdownOption.Combine(option.Markdown, MarkdownOption.Default); + Option.Output = OutputOption.Combine(option.Output, OutputOption.Default); + + if (!string.IsNullOrEmpty(Option.Output.Path)) + OutputVisitor = (o, enumerate) => WriteToFile(o, Option, Writer, ShouldProcess); + + ConfigureCulture(); + return this; + } + + public abstract IPipeline Build(); + + /// + /// Require sources for pipeline execution. + /// + /// Returns true when the condition is not met. + protected bool RequireSources() + { + if (Source == null || Source.Length == 0) + { + Writer.WarnSourcePathNotFound(); + return true; + } + return false; + } + + /// + /// Require culture for pipeline exeuction. + /// + /// Returns true when the condition is not met. + protected bool RequireCulture() + { + if (Option.Output.Culture == null || Option.Output.Culture.Length == 0) + { + Writer.ErrorInvariantCulture(); + return true; + } + return false; + } + + protected virtual PipelineContext PrepareContext() + { + return new PipelineContext(GetOptionContext(), PrepareStream(), Writer, OutputVisitor, null, null); + } + + protected virtual OptionContext GetOptionContext() + { + return new OptionContext(Option); + } + + protected virtual PipelineStream PrepareStream() + { + return new PipelineStream(null, null); + } + + private static void WriteToFile(IDocumentResult result, PSDocumentOption option, IPipelineWriter writer, ShouldProcess shouldProcess) + { + // Calculate paths + var fileName = string.Concat(result.InstanceName, result.Extension); + var outputPath = PSDocumentOption.GetRootedPath(result.OutputPath); + var filePath = Path.Combine(outputPath, fileName); + var parentPath = Directory.GetParent(filePath); + + if (!parentPath.Exists && shouldProcess(target: parentPath.FullName, action: PSDocsResources.ShouldCreatePath)) + Directory.CreateDirectory(path: parentPath.FullName); + + if (shouldProcess(target: outputPath, action: PSDocsResources.ShouldWriteFile)) + { + var encoding = GetEncoding(option.Markdown.Encoding.Value); + File.WriteAllText(filePath, result.ToString(), encoding); + + // Write file info instead + var fileInfo = new FileInfo(filePath); + writer.WriteObject(fileInfo, false); + } + } + + private static void WriteToString(IDocumentResult result, bool enumerate, IPipelineWriter writer) + { + writer.WriteObject(result.ToString(), enumerate); + } + + private static Encoding GetEncoding(MarkdownEncoding encoding) + { + switch (encoding) + { + case MarkdownEncoding.UTF7: + return Encoding.UTF7; + case MarkdownEncoding.UTF8: + return Encoding.UTF8; + case MarkdownEncoding.ASCII: + return Encoding.ASCII; + case MarkdownEncoding.Unicode: + return Encoding.Unicode; + case MarkdownEncoding.UTF32: + return Encoding.UTF32; + default: + return new UTF8Encoding(false); + } + } + + private void ConfigureCulture() + { + if (Option.Output.Culture == null || Option.Output.Culture.Length == 0) + { + // Fallback to current culture + var current = PSDocumentOption.GetCurrentCulture(); + if (current == null || string.IsNullOrEmpty(current.Name)) + return; + + Option.Output.Culture = new string[] { current.Name }; + } + } + + protected void AddVisitTargetObjectAction(VisitTargetObjectAction action) + { + // Nest the previous write action in the new supplied action + // Execution chain will be: action -> previous -> previous..n + var previous = VisitTargetObject; + VisitTargetObject = (targetObject) => action(targetObject, previous); + } + } + + internal abstract class PipelineBase : IDisposable + { + protected readonly PipelineContext Context; + protected readonly Source[] Source; + + // Track whether Dispose has been called. + private bool _Disposed; + + protected PipelineBase(PipelineContext context, Source[] source) + { + Context = context; + Source = source; + } + + public virtual void Begin() + { + // Do nothing + } + + public virtual void Process(PSObject sourceObject) + { + // Do nothing + } + + public virtual void End() + { + // Do nothing + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + Context.Dispose(); + } + _Disposed = true; + } + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PipelineContext.cs b/packages/psdocs/src/PSDocs/Pipeline/PipelineContext.cs new file mode 100644 index 00000000..f43a8c12 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PipelineContext.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using PSDocs.Configuration; +using PSDocs.Definitions; +using PSDocs.Definitions.Selectors; +using PSDocs.Models; +using PSDocs.Processor; + +namespace PSDocs.Pipeline +{ + /// + /// A context for an end-to-end pipeline execution. + /// + internal sealed class PipelineContext : IDisposable + { + internal readonly OptionContext Option; + internal readonly LanguageMode LanguageMode; + internal readonly DocumentFilter Filter; + internal readonly PipelineStream Stream; + internal readonly IPipelineWriter Writer; + internal readonly InstanceNameBinder InstanceNameBinder; + internal readonly string[] Convention; + internal readonly Dictionary Selector; + + private readonly Action _OutputVisitor; + + // Track whether Dispose has been called. + private bool _Disposed; + + public PipelineContext(OptionContext option, PipelineStream stream, IPipelineWriter writer, Action _Output, InstanceNameBinder instanceNameBinder, string[] convention) + { + Option = option; + LanguageMode = option.Execution.LanguageMode.GetValueOrDefault(ExecutionOption.Default.LanguageMode.Value); + Filter = DocumentFilter.Create(option.Document.Include, option.Document.Tag); + Stream = stream ?? new PipelineStream(null, null); + Writer = writer; + InstanceNameBinder = instanceNameBinder; + _OutputVisitor = _Output; + Convention = convention; + Selector = new Dictionary(); + } + + internal void WriteOutput(IDocumentResult result) + { + _OutputVisitor(result, false); + } + + internal void Import(IResource resource) + { + if (resource.Kind == ResourceKind.Selector && resource is SelectorV1 selector) + Selector[selector.Id] = new SelectorVisitor(selector.Id, selector.Spec.If); + } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + //if (disposing) + //{ + + //} + _Disposed = true; + } + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PipelineReceiver.cs b/packages/psdocs/src/PSDocs/Pipeline/PipelineReceiver.cs new file mode 100644 index 00000000..d7f3b021 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PipelineReceiver.cs @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Management.Automation; +using System.Net; +using Newtonsoft.Json; +using PSDocs.Configuration; +using PSDocs.Data; +using YamlDotNet.Core; +using YamlDotNet.Core.Events; +using YamlDotNet.Serialization; + +namespace PSDocs.Pipeline +{ + internal delegate IEnumerable VisitTargetObject(TargetObject targetObject); + internal delegate IEnumerable VisitTargetObjectAction(TargetObject targetObject, VisitTargetObject next); + + internal static class PipelineReceiverActions + { + private static readonly TargetObject[] EmptyArray = Array.Empty(); + + public static IEnumerable PassThru(TargetObject targetObject) + { + yield return targetObject; + } + + public static IEnumerable DetectInputFormat(TargetObject targetObject, VisitTargetObject next) + { + var pathExtension = GetPathExtension(targetObject); + + // Handle JSON + if (pathExtension == ".json" || pathExtension == ".jsonc") + { + return ConvertFromJson(targetObject, next); + } + // Handle YAML + else if (pathExtension == ".yaml" || pathExtension == ".yml") + { + return ConvertFromYaml(targetObject, next); + } + // Handle PowerShell Data + else if (pathExtension == ".psd1") + { + return ConvertFromPowerShellData(targetObject, next); + } + return new TargetObject[] { targetObject }; + } + + public static IEnumerable ConvertFromJson(TargetObject targetObject, VisitTargetObject next) + { + // Only attempt to deserialize if the input is a string, file or URI + if (!IsAcceptedType(targetObject)) + return new TargetObject[] { targetObject }; + + var json = ReadAsString(targetObject.Value, out var sourceInfo); + var value = JsonConvert.DeserializeObject(json, new PSObjectArrayJsonConverter()); + return VisitItems(value, sourceInfo, next); + } + + public static IEnumerable ConvertFromYaml(TargetObject targetObject, VisitTargetObject next) + { + // Only attempt to deserialize if the input is a string, file or URI + if (!IsAcceptedType(targetObject)) + return new TargetObject[] { targetObject }; + + var d = new DeserializerBuilder() + .IgnoreUnmatchedProperties() + .WithTypeConverter(new PSObjectYamlTypeConverter()) + .WithNodeTypeResolver(new PSObjectYamlTypeResolver()) + .Build(); + + var reader = ReadAsReader(targetObject.Value, out var sourceInfo); + var parser = new YamlDotNet.Core.Parser(reader); + var result = new List(); + parser.TryConsume(out _); + while (parser.Current is DocumentStart) + { + var item = d.Deserialize(parser: parser); + if (item == null) + continue; + + result.AddRange(VisitItem(item, sourceInfo, next)); + } + return result.Count == 0 ? EmptyArray : result.ToArray(); + } + + public static IEnumerable ConvertFromPowerShellData(TargetObject targetObject, VisitTargetObject next) + { + // Only attempt to deserialize if the input is a string or a file + if (!IsAcceptedType(targetObject)) + return new TargetObject[] { targetObject }; + + var data = ReadAsString(targetObject.Value, out var sourceInfo); + var ast = System.Management.Automation.Language.Parser.ParseInput(data, out _, out _); + var hashtables = ast.FindAll(item => item is System.Management.Automation.Language.HashtableAst, false); + if (hashtables == null) + return EmptyArray; + + var result = new List(); + foreach (var hashtable in hashtables) + { + if (hashtable?.Parent?.Parent?.Parent?.Parent == ast) + result.Add(PSObject.AsPSObject(hashtable.SafeGetValue())); + } + return VisitItems(result, sourceInfo, next); + } + + public static IEnumerable ReadObjectPath(TargetObject targetObject, VisitTargetObject source, string objectPath, bool caseSensitive) + { + if (!ObjectHelper.GetField(bindingContext: null, targetObject: targetObject, name: objectPath, caseSensitive: caseSensitive, value: out var nestedObject)) + return EmptyArray; + + var nestedType = nestedObject.GetType(); + if (typeof(IEnumerable).IsAssignableFrom(nestedType)) + { + var result = new List(); + foreach (var item in (nestedObject as IEnumerable)) + result.Add(new TargetObject(PSObject.AsPSObject(item))); + + return result.ToArray(); + } + else + { + return new TargetObject[] { new TargetObject(PSObject.AsPSObject(nestedObject)) }; + } + } + + private static string GetPathExtension(TargetObject targetObject) + { + if (targetObject.Value.BaseObject is InputFileInfo inputFileInfo) + return inputFileInfo.Extension; + + if (targetObject.Value.BaseObject is FileInfo fileInfo) + return fileInfo.Extension; + + if (targetObject.Value.BaseObject is Uri uri) + return Path.GetExtension(uri.OriginalString); + + return null; + } + + private static bool IsAcceptedType(TargetObject targetObject) + { + return targetObject.Value.BaseObject is string || + targetObject.Value.BaseObject is InputFileInfo || + targetObject.Value.BaseObject is FileInfo || + targetObject.Value.BaseObject is Uri; + } + + private static string ReadAsString(PSObject sourceObject, out InputFileInfo sourceInfo) + { + sourceInfo = null; + if (sourceObject.BaseObject is string) + { + return sourceObject.BaseObject.ToString(); + } + else if (sourceObject.BaseObject is InputFileInfo inputFileInfo) + { + sourceInfo = inputFileInfo; + using var reader = new StreamReader(inputFileInfo.FullName); + return reader.ReadToEnd(); + } + else if (sourceObject.BaseObject is FileInfo fileInfo) + { + sourceInfo = new InputFileInfo(PSDocumentOption.GetRootedBasePath(""), fileInfo.FullName); + using var reader = new StreamReader(fileInfo.FullName); + return reader.ReadToEnd(); + } + else + { + var uri = sourceObject.BaseObject as Uri; + sourceInfo = new InputFileInfo(null, uri.ToString()); + using var webClient = new WebClient(); + return webClient.DownloadString(uri); + } + } + + private static TextReader ReadAsReader(PSObject sourceObject, out InputFileInfo sourceInfo) + { + sourceInfo = null; + if (sourceObject.BaseObject is string) + { + return new StringReader(sourceObject.BaseObject.ToString()); + } + else if (sourceObject.BaseObject is InputFileInfo inputFileInfo) + { + sourceInfo = inputFileInfo; + return new StreamReader(inputFileInfo.FullName); + } + else if (sourceObject.BaseObject is FileInfo fileInfo) + { + sourceInfo = new InputFileInfo(PSDocumentOption.GetRootedBasePath(""), fileInfo.FullName); + return new StreamReader(fileInfo.FullName); + } + else + { + var uri = sourceObject.BaseObject as Uri; + sourceInfo = new InputFileInfo(null, uri.ToString()); + using var webClient = new WebClient(); + return new StringReader(webClient.DownloadString(uri)); + } + } + + private static IEnumerable VisitItem(PSObject value, InputFileInfo sourceInfo, VisitTargetObject next) + { + if (value == null) + return EmptyArray; + + var items = next(new TargetObject(value)); + if (items == null) + return EmptyArray; + + foreach (var i in items) + NoteSource(i, sourceInfo); + + return items; + } + + private static IEnumerable VisitItems(IEnumerable value, InputFileInfo sourceInfo, VisitTargetObject next) + { + if (value == null) + return EmptyArray; + + var result = new List(); + foreach (var item in value) + result.AddRange(VisitItem(item, sourceInfo, next)); + + return result.Count == 0 ? EmptyArray : result.ToArray(); + } + + private static void NoteSource(TargetObject value, InputFileInfo source) + { + if (value == null || source == null) + return; + + value.SetSourceInfo(source); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PipelineStream.cs b/packages/psdocs/src/PSDocs/Pipeline/PipelineStream.cs new file mode 100644 index 00000000..64a03676 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PipelineStream.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.Management.Automation; +using PSDocs.Data; + +namespace PSDocs.Pipeline +{ + internal sealed class PipelineStream + { + private readonly VisitTargetObject _Input; + private readonly InputFileInfo[] _InputPath; + private readonly ConcurrentQueue _Queue; + + public PipelineStream(VisitTargetObject input, InputFileInfo[] inputPath) + { + _Input = input; + _InputPath = inputPath; + _Queue = new ConcurrentQueue(); + } + + public int Count => _Queue.Count; + + public bool IsEmpty => _Queue.IsEmpty; + + public void Enqueue(PSObject sourceObject) + { + if (sourceObject == null) + return; + + var targetObject = new TargetObject(sourceObject); + if (_Input == null) + { + _Queue.Enqueue(targetObject); + return; + } + + // Visit the object, which may change or expand the object + var input = _Input(targetObject); + if (input == null) + return; + + foreach (var item in input) + _Queue.Enqueue(item); + } + + public bool TryDequeue(out TargetObject targetObject) + { + return _Queue.TryDequeue(out targetObject); + } + + public void Open() + { + if (_InputPath == null || _InputPath.Length == 0) + return; + + // Read each file + for (var i = 0; i < _InputPath.Length; i++) + { + if (_InputPath[i].IsUrl) + { + Enqueue(PSObject.AsPSObject(new Uri(_InputPath[i].FullName))); + } + else + { + Enqueue(PSObject.AsPSObject(_InputPath[i])); + } + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/PipelineWriter.cs b/packages/psdocs/src/PSDocs/Pipeline/PipelineWriter.cs new file mode 100644 index 00000000..0d66d323 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/PipelineWriter.cs @@ -0,0 +1,391 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Threading; +using PSDocs.Resources; + +namespace PSDocs.Pipeline +{ + public interface IPipelineWriter : IPipelineDisposable + { + void WriteError(ErrorRecord record); + + void WriteWarning(string text, params object[] args); + + void WriteInformation(InformationRecord record); + + void WriteVerbose(string text, params object[] args); + + void WriteDebug(DebugRecord record); + + void WriteDebug(string text, params object[] args); + + void WriteObject(object sendToPipeline, bool enumerateCollection); + + void WriteHost(HostInformationMessage info); + } + + public static class PipelineWriterExtensions + { + [DebuggerStepThrough] + public static void WriteError(this IPipelineWriter writer, Exception exception, string errorId, ErrorCategory errorCategory, object targetObject) + { + if (writer == null) + return; + + writer.WriteError(new ErrorRecord(exception, errorId, errorCategory, targetObject)); + } + + public static void Debug(this IPipelineWriter writer, string message) + { + if (writer == null) + return; + + writer.WriteDebug(new DebugRecord(message)); + } + + public static void WarnSourcePathNotFound(this IPipelineWriter writer) + { + if (writer == null) + return; + + writer.WriteWarning(PSDocsResources.SourceNotFound); + } + + public static void WarnTitleEmpty(this IPipelineWriter writer) + { + if (writer == null) + return; + + writer.WriteWarning(PSDocsResources.TitleEmpty); + } + + public static void ErrorInvariantCulture(this IPipelineWriter writer) + { + if (writer == null) + return; + + writer.WriteError(new ErrorRecord( + exception: new PipelineBuilderException(PSDocsResources.InvariantCulture), + errorId: "PSDocs.PipelineBuilder.InvariantCulture", + errorCategory: ErrorCategory.InvalidOperation, + null + )); + } + + public static void WriteError(this IPipelineWriter writer, ParseError error) + { + if (writer == null) + return; + + var record = new ErrorRecord + ( + exception: new ParseException(message: error.Message, errorId: error.ErrorId), + errorId: error.ErrorId, + errorCategory: ErrorCategory.InvalidOperation, + targetObject: null + ); + writer.WriteError(record); + } + } + + /// + /// A base class for implementing IPipelineWriter. + /// + public abstract class PipelineWriterBase + { + protected PipelineWriterBase(IHostContext hostContext) + { + HostContext = hostContext; + } + + protected IHostContext HostContext { get; } + + public virtual void Begin() + { + // Do nothing + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] + public virtual void End() + { + // Do nothing + } + + public void WriteError(ErrorRecord record) + { + if (record == null || !ShouldWriteError()) + return; + + DoWriteError(record); + } + + public void WriteWarning(string text, params object[] args) + { + if (string.IsNullOrEmpty(text) || !ShouldWriteWarning()) + return; + + text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); + DoWriteWarning(text); + } + + public void WriteInformation(InformationRecord record) + { + if (record == null || !ShouldWriteInformation()) + return; + + DoWriteInformation(record); + } + + public void WriteVerbose(string text, params object[] args) + { + if (string.IsNullOrEmpty(text) || !ShouldWriteVerbose()) + return; + + text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); + DoWriteVerbose(text); + } + + public void WriteDebug(DebugRecord record) + { + if (record == null || !ShouldWriteDebug()) + return; + + DoWriteDebug(record); + } + + public void WriteDebug(string text, params object[] args) + { + if (string.IsNullOrEmpty(text) || !ShouldWriteDebug()) + return; + + text = args == null || args.Length == 0 ? text : string.Format(Thread.CurrentThread.CurrentCulture, text, args); + DoWriteDebug(new DebugRecord(text)); + } + + public void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (sendToPipeline == null || !ShouldWriteObject()) + return; + + DoWriteObject(sendToPipeline, enumerateCollection); + } + + public void WriteHost(HostInformationMessage info) + { + if (info == null) + return; + + DoWriteHost(info); + } + + protected virtual bool ShouldWriteError() + { + return HostContext.GetErrorPreference() != ActionPreference.Ignore; + } + + protected virtual bool ShouldWriteWarning() + { + return HostContext.GetWarningPreference() != ActionPreference.Ignore; + } + + protected virtual bool ShouldWriteInformation() + { + return HostContext.GetInformationPreference() != ActionPreference.Ignore; + } + + protected virtual bool ShouldWriteVerbose() + { + return HostContext.GetVerbosePreference() != ActionPreference.Ignore; + } + + protected virtual bool ShouldWriteDebug() + { + return HostContext.GetDebugPreference() != ActionPreference.Ignore; + } + + protected virtual bool ShouldWriteObject() + { + return true; + } + + protected virtual void DoWriteError(ErrorRecord record) + { + + } + + protected virtual void DoWriteWarning(string text) + { + + } + + protected virtual void DoWriteInformation(InformationRecord record) + { + + } + + protected virtual void DoWriteVerbose(string text) + { + + } + + protected virtual void DoWriteDebug(DebugRecord record) + { + + } + + protected virtual void DoWriteObject(object sendToPipeline, bool enumerateCollection) + { + + } + + protected virtual void DoWriteHost(HostInformationMessage info) + { + + } + } + + public abstract class PipelineWriter : PipelineWriterBase + { + private readonly IPipelineWriter _Inner; + + protected PipelineWriter(IHostContext hostContext, IPipelineWriter inner) + : base(hostContext) + { + _Inner = inner; + } + + public override void Begin() + { + if (_Inner == null) + return; + + _Inner.Begin(); + } + + public override void End() + { + if (_Inner == null) + return; + + _Inner.End(); + } + + //public void WriteVerbose(string format, params object[] args) + //{ + // if (!ShouldWriteVerbose()) + // return; + + // WriteVerbose(string.Format(Thread.CurrentThread.CurrentCulture, format, args)); + //} + + protected override void DoWriteError(ErrorRecord errorRecord) + { + if (_Inner == null) + return; + + _Inner.WriteError(errorRecord); + } + + protected override void DoWriteWarning(string message) + { + if (_Inner == null) + return; + + _Inner.WriteWarning(message); + } + + protected override void DoWriteInformation(InformationRecord informationRecord) + { + if (_Inner == null) + return; + + _Inner.WriteInformation(informationRecord); + } + + protected override void DoWriteVerbose(string message) + { + if (_Inner == null) + return; + + _Inner.WriteVerbose(message); + } + + protected override void DoWriteDebug(DebugRecord debugRecord) + { + if (_Inner == null || debugRecord == null) + return; + + _Inner.WriteDebug(debugRecord); + } + + protected override void DoWriteObject(object sendToPipeline, bool enumerateCollection) + { + if (_Inner == null) + return; + + _Inner.WriteObject(sendToPipeline, enumerateCollection); + } + + protected override void DoWriteHost(HostInformationMessage info) + { + if (_Inner == null) + return; + + _Inner.WriteHost(info); + } + } + + internal abstract class BufferedPipelineWriter : PipelineWriter + { + private readonly List _Result; + + protected BufferedPipelineWriter(IHostContext hostContext, IPipelineWriter inner) + : base(hostContext, inner) + { + _Result = new List(); + } + + protected override void DoWriteObject(object sendToPipeline, bool enumerateCollection) + { + Add(sendToPipeline); + if (ShouldFlush()) + Flush(); + } + + protected void Add(object o) + { + if (o is T[] collection) + _Result.AddRange(collection); + else if (o is T item) + _Result.Add(item); + } + + public void Flush() + { + var results = _Result.ToArray(); + _Result.Clear(); + Flush(results); + } + + public sealed override void End() + { + Flush(); + } + + protected virtual bool ShouldFlush() + { + return false; + } + + protected virtual void Flush(T[] o) + { + base.WriteObject(o, true); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/SourcePipeline.cs b/packages/psdocs/src/PSDocs/Pipeline/SourcePipeline.cs new file mode 100644 index 00000000..65ad975d --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/SourcePipeline.cs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Pipeline.Output; +using PSDocs.Resources; + +namespace PSDocs.Pipeline +{ + public enum SourceType + { + Script = 1, + + Yaml = 2 + } + + /// + /// A source file for document definitions. + /// + [DebuggerDisplay("{Type}: {Path}")] + public sealed class SourceFile + { + public readonly string Path; + public readonly string ModuleName; + public readonly SourceType Type; + + public SourceFile(string path, string moduleName, SourceType type, string helpPath) + { + Path = path; + ModuleName = moduleName; + Type = type; + ResourcePath = helpPath; + } + + public string ResourcePath { get; } + } + + public sealed class Source + { + public readonly string Path; + public readonly SourceFile[] File; + internal readonly ModuleInfo Module; + + internal Source(string path, SourceFile[] file) + { + Path = path; + Module = null; + File = file; + } + + internal Source(ModuleInfo module, SourceFile[] file) + { + Path = module.Path; + Module = module; + File = file; + } + + internal sealed class ModuleInfo + { + public readonly string Path; + public readonly string Name; + + public ModuleInfo(PSModuleInfo info) + { + Path = info.ModuleBase; + Name = info.Name; + } + } + } + + /// + /// A helper to build a list of document sources for discovery. + /// + public sealed class SourcePipelineBuilder + { + private readonly List _Source; + private readonly HostContext _HostContext; + private readonly IPipelineWriter _Writer; + + internal SourcePipelineBuilder(HostContext hostContext) + { + _Source = new List(); + _HostContext = hostContext; + _Writer = new HostPipelineWriter(hostContext); + } + + public bool ShouldLoadModule => _HostContext.GetAutoLoadingPreference() == PSModuleAutoLoadingPreference.All; + + private void VerboseScanSource(string path) + { + _Writer.WriteVerbose(PSDocsResources.ScanSource, path); + } + + private void VerboseFoundModules(int count) + { + _Writer.WriteVerbose(PSDocsResources.FoundModules, count); + } + + private void VerboseScanModule(string moduleName) + { + _Writer.WriteVerbose(PSDocsResources.ScanModule, moduleName); + } + + /// + /// Add loose files as a source. + /// + /// A file or directory path containing one or more document files. + public void Directory(string[] path) + { + if (path == null || path.Length == 0) + return; + + for (var i = 0; i < path.Length; i++) + Directory(path[i]); + } + + public void Directory(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + VerboseScanSource(path); + var files = GetFiles(path, null); + + if (files == null || files.Length == 0) + return; + + Source(new Source(path, files)); + } + + /// + /// Add a module source. + /// + /// + public void Module(PSModuleInfo[] module) + { + if (module == null) + return; + + for (var i = 0; i < module.Length; i++) + { + VerboseScanModule(module[i].Name); + var files = GetFiles(module[i].ModuleBase, module[i].ModuleBase, module[i].Name); + + if (files == null || files.Length == 0) + continue; + + Source(new Source(new Source.ModuleInfo(module[i]), file: files)); + } + } + + public Source[] Build() + { + return _Source.ToArray(); + } + + private void Source(Source source) + { + _Source.Add(source); + } + + private static SourceFile[] GetFiles(string path, string helpPath, string moduleName = null) + { + var result = new List(); + var rootedPath = PSDocumentOption.GetRootedPath(path); + var extension = Path.GetExtension(rootedPath); + if (extension == ".ps1" || extension == ".yaml" || extension == ".yml") + { + if (!File.Exists(rootedPath)) + throw new FileNotFoundException(PSDocsResources.SourceNotFound, rootedPath); + + if (helpPath == null) + helpPath = Path.GetDirectoryName(rootedPath); + + result.Add(new SourceFile(rootedPath, null, GetSourceType(rootedPath), helpPath)); + } + else if (System.IO.Directory.Exists(rootedPath)) + { + var files = System.IO.Directory.EnumerateFiles(rootedPath, "*.Doc.*", SearchOption.AllDirectories); + foreach (var file in files) + { + if (Include(file)) + { + if (helpPath == null) + helpPath = Path.GetDirectoryName(file); + + result.Add(new SourceFile(file, moduleName, GetSourceType(file), helpPath)); + } + } + } + else + { + return null; + } + return result.ToArray(); + } + + private static bool Include(string path) + { + return path.EndsWith(".Doc.ps1", System.StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".Doc.yaml", System.StringComparison.OrdinalIgnoreCase) || + path.EndsWith(".Doc.yml", System.StringComparison.OrdinalIgnoreCase); + } + + private static SourceType GetSourceType(string path) + { + var extension = Path.GetExtension(path); + return (extension == ".yaml" || extension == ".yml") ? SourceType.Yaml : SourceType.Script; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/StreamPipeline.cs b/packages/psdocs/src/PSDocs/Pipeline/StreamPipeline.cs new file mode 100644 index 00000000..3970c2d0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/StreamPipeline.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Pipeline +{ + internal abstract class StreamPipeline : PipelineBase + { + private readonly PipelineStream _Stream; + + internal StreamPipeline(PipelineContext context, Source[] source) + : base(context, source) + { + _Stream = context.Stream; + } + + public override void Begin() + { + _Stream.Open(); + } + + public sealed override void Process(PSObject sourceObject) + { + _Stream.Enqueue(sourceObject); + while (!_Stream.IsEmpty && _Stream.TryDequeue(out var nextObject)) + ProcessObject(nextObject); + } + + protected abstract void ProcessObject(TargetObject targetObject); + } +} diff --git a/packages/psdocs/src/PSDocs/Pipeline/TargetObject.cs b/packages/psdocs/src/PSDocs/Pipeline/TargetObject.cs new file mode 100644 index 00000000..6a56f07b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Pipeline/TargetObject.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation; +using PSDocs.Data; +using PSDocs.Definitions.Selectors; + +namespace PSDocs.Pipeline +{ + internal abstract class TargetObjectAnnotation + { + + } + + internal sealed class SelectorTargetAnnotation : TargetObjectAnnotation + { + private readonly Dictionary _Results; + + public SelectorTargetAnnotation() + { + _Results = new Dictionary(); + } + + public bool TryGetSelectorResult(SelectorVisitor selector, out bool result) + { + return _Results.TryGetValue(selector.InstanceId, out result); + } + + public void SetSelectorResult(SelectorVisitor selector, bool result) + { + _Results[selector.InstanceId] = result; + } + } + + internal sealed class TargetObject + { + private readonly Dictionary _Annotations; + + internal TargetObject(PSObject o) + { + Value = o; + _Annotations = new Dictionary(); + } + + public PSObject Value { get; } + + public InputFileInfo Source { get; private set; } + + public T GetAnnotation() where T : TargetObjectAnnotation, new() + { + if (!_Annotations.TryGetValue(typeof(T), out var value)) + { + value = new T(); + _Annotations.Add(typeof(T), value); + } + return (T)value; + } + + internal void SetSourceInfo(InputFileInfo sourceInfo) + { + Source = sourceInfo; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Processor/IDocumentProcessor.cs b/packages/psdocs/src/PSDocs/Processor/IDocumentProcessor.cs new file mode 100644 index 00000000..a1d74ee4 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Processor/IDocumentProcessor.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Configuration; +using PSDocs.Models; + +namespace PSDocs.Processor +{ + public interface IDocumentProcessor + { + void Process(PSDocumentOption option, Document document); + } +} diff --git a/packages/psdocs/src/PSDocs/Processor/IDocumentResult.cs b/packages/psdocs/src/PSDocs/Processor/IDocumentResult.cs new file mode 100644 index 00000000..af3f110b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Processor/IDocumentResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Specialized; + +namespace PSDocs.Processor +{ + internal interface IDocumentResult + { + string InstanceName { get; } + + string Extension { get; } + + string Culture { get; } + + string OutputPath { get; } + + string FullName { get; } + + OrderedDictionary Metadata { get; } + + Hashtable Data { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessor.cs b/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessor.cs new file mode 100644 index 00000000..a0494bcf --- /dev/null +++ b/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessor.cs @@ -0,0 +1,369 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Specialized; +using System.Diagnostics; +using System.IO; +using PSDocs.Configuration; +using PSDocs.Models; +using PSDocs.Runtime; + +namespace PSDocs.Processor.Markdown +{ + internal sealed class MarkdownProcessor + { + private const string MARKDOWN_BLOCKQUOTE = "> "; + + /// + /// A markdown document result. + /// + private sealed class DocumentResult : IDocumentResult + { + private const string DEFAULT_EXTENSION = ".md"; + + private readonly string _Markdown; + + internal DocumentResult(DocumentContext documentContext, string markdown) + { + _Markdown = markdown; + InstanceName = documentContext.InstanceName; + Culture = documentContext.Culture; + OutputPath = PSDocumentOption.GetRootedPath(documentContext.OutputPath); + FullName = GetFullName(); + Metadata = documentContext.Metadata; + Data = documentContext.Data; + } + + [DebuggerStepThrough] + public override string ToString() + { + return _Markdown; + } + + public string InstanceName { get; } + + public string Culture { get; } + + public string Extension => DEFAULT_EXTENSION; + + public string OutputPath { get; } + + public string FullName { get; } + + public OrderedDictionary Metadata { get; } + + public Hashtable Data { get; } + + private string GetFullName() + { + var fileName = string.Concat(InstanceName, Extension); + return Path.Combine(OutputPath, fileName); + } + } + + public IDocumentResult Process(IPSDocumentOption option, Document document) + { + if (document == null) + return null; + + var context = new MarkdownProcessorContext(option, document); + Document(context); + return new DocumentResult(document.Context, markdown: context.GetString()); + } + + private static void Document(MarkdownProcessorContext context) + { + // Process metadata + Metadata(context); + + if (!string.IsNullOrEmpty(context.Document.Title)) + { + context.WriteHeaderHash(1); + context.WriteLine(context.Document.Title); + context.LineBreak(); + } + + foreach (var node in context.Document.Node) + { + Node(context, node); + } + context.EndDocument(); + } + + private static void Metadata(MarkdownProcessorContext context) + { + // Only write metadata block if there is at least one metadata property set + if (context.Document.Metadata == null || context.Document.Metadata.Count == 0) + return; + + context.WriteFrontMatter(); + foreach (var key in context.Document.Metadata.Keys) + { + context.WriteLine(key.ToString(), ": ", context.Document.Metadata[key].ToString()); + } + context.WriteFrontMatter(); + context.LineBreak(); + } + + private static void Node(MarkdownProcessorContext context, object node) + { + if (node == null) + return; + + if (node is DocumentNode documentNode) + { + switch (documentNode.Type) + { + case DocumentNodeType.Section: + Section(context, documentNode as Section); + break; + + case DocumentNodeType.Table: + Table(context, documentNode as Table); + break; + + case DocumentNodeType.Code: + Code(context, documentNode as Code); + break; + + case DocumentNodeType.BlockQuote: + BlockQuote(context, documentNode as BlockQuote); + break; + + case DocumentNodeType.Text: + Text(context, documentNode as Text); + break; + + case DocumentNodeType.Include: + Include(context, documentNode as Include); + break; + } + return; + } + String(context, node.ToString()); + } + + private static void String(MarkdownProcessorContext context, string node) + { + context.WriteLine(node); + } + + private static void Section(MarkdownProcessorContext context, Section section) + { + context.WriteHeaderHash(section.Level); + context.Write(section.Title); + context.LineBreak(); + if (section.Node.Count > 0) + { + foreach (var node in section.Node) + Node(context, node); + } + } + + private static void BlockQuote(MarkdownProcessorContext context, BlockQuote node) + { + if (!string.IsNullOrEmpty(node.Info)) + { + context.Write(MARKDOWN_BLOCKQUOTE); + context.Write("[!"); + context.Write(node.Info.ToUpper()); + context.WriteLine("]"); + } + if (!string.IsNullOrEmpty(node.Title)) + { + context.Write(MARKDOWN_BLOCKQUOTE); + context.WriteLine(node.Title); + } + foreach (var line in node.Content) + { + context.Write(MARKDOWN_BLOCKQUOTE); + context.WriteLine(line); + } + context.LineBreak(); + } + + private static void Code(MarkdownProcessorContext context, Code code) + { + context.WriteTripleBacktick(); + if (!string.IsNullOrEmpty(code.Info)) + context.Write(code.Info); + + context.Ending(); + context.WriteLine(code.Content); + context.WriteTripleBacktick(); + context.LineBreak(); + } + + private static void Text(MarkdownProcessorContext context, Text text) + { + context.WriteLine(text.Content); + context.LineBreak(); + } + + private static void Include(MarkdownProcessorContext context, Include include) + { + context.WriteLine(include.Content); + } + + private static void Table(MarkdownProcessorContext context, Table table) + { + if (table.Headers == null || table.Headers.Count == 0) + return; + + var lastHeader = table.Headers.Count - 1; + var useEdgePipe = context.Option.Markdown.UseEdgePipes == EdgePipeOption.Always + || table.Headers.Count == 1; + var padColumn = context.Option.Markdown.ColumnPadding == ColumnPadding.Single + || context.Option.Markdown.ColumnPadding == ColumnPadding.MatchHeader; + + // Write table headers + for (var i = 0; i < table.Headers.Count; i++) + { + StartColumn(context, i, lastHeader, useEdgePipe, padColumn, padColumn); + + context.Write(table.Headers[i].Label); + + if (i < lastHeader) + { + var padding = 0; + + // Pad column + if (table.Headers[i].Width > 0 && (table.Headers[i].Width - table.Headers[i].Label.Length) > 0) + { + padding = table.Headers[i].Width - table.Headers[i].Label.Length; + } + context.WriteSpace(padding); + } + } + + if (padColumn && useEdgePipe) + context.WriteSpace(); + + context.WriteLine(useEdgePipe ? "|" : string.Empty); + + // Write table header separator + for (var i = 0; i < table.Headers.Count; i++) + { + StartColumn(context, i, lastHeader, useEdgePipe, padColumn, padColumn); + + switch (table.Headers[i].Alignment) + { + case Alignment.Left: + context.Write(":"); + context.Write('-', table.Headers[i].Label.Length - 1); + break; + + case Alignment.Right: + context.Write('-', table.Headers[i].Label.Length - 1); + context.Write(":"); + break; + + case Alignment.Center: + context.Write(":"); + context.Write('-', table.Headers[i].Label.Length - 2); + context.Write(":"); + break; + + default: + context.Write('-', table.Headers[i].Label.Length); + break; + } + + if (i < lastHeader) + { + var padding = 0; + + // Pad column + if (table.Headers[i].Width > 0 && (table.Headers[i].Width - table.Headers[i].Label.Length) > 0) + { + padding = table.Headers[i].Width - table.Headers[i].Label.Length; + } + + context.WriteSpace(padding); + } + } + + if (padColumn && useEdgePipe) + { + context.WriteSpace(); + } + + context.WriteLine(useEdgePipe ? "|" : string.Empty); + + // Write table rows + for (var r = 0; r < table.Rows.Count; r++) + { + for (var c = 0; c < table.Rows[r].Length; c++) + { + var text = WrapText(context, table.Rows[r][c]); + + StartColumn(context, c, lastHeader, useEdgePipe, padBeforePipe: padColumn, padAfterPipe: padColumn && (c < lastHeader || !string.IsNullOrEmpty(text))); + + context.Write(text); + + if (c < lastHeader) + { + var padding = 0; + + // Pad column using column width + if (table.Headers[c].Width > 0 && (table.Headers[c].Width - text.Length) > 0) + { + padding = table.Headers[c].Width - text.Length; + } + // Pad column matching header + else if (context.Option.Markdown.ColumnPadding == ColumnPadding.MatchHeader) + { + if ((table.Headers[c].Label.Length - text.Length) > 0) + { + padding = table.Headers[c].Label.Length - text.Length; + } + } + + context.WriteSpace(padding); + } + } + + if (padColumn && useEdgePipe) + { + context.WriteSpace(); + } + + context.WriteLine(useEdgePipe ? "|" : string.Empty); + } + context.LineBreak(); + } + + private static void StartColumn(MarkdownProcessorContext context, int index, int last, bool useEdgePipe, bool padBeforePipe, bool padAfterPipe) + { + if (index > 0 && padBeforePipe) + { + context.WriteSpace(); + } + if (index > 0 || useEdgePipe) + { + context.WritePipe(); + } + if (padAfterPipe && useEdgePipe || index > 0 && padAfterPipe) + { + context.WriteSpace(); + } + } + + private static string WrapText(MarkdownProcessorContext context, string text) + { + var separator = context.Option.Markdown.WrapSeparator; + var formatted = text; + + if (text == null) + { + return string.Empty; + } + if (text.Contains("\n") || text.Contains("\r")) + { + formatted = text.Replace("\r\n", separator).Replace("\n", separator).Replace("\r", separator); + } + return formatted; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessorContext.cs b/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessorContext.cs new file mode 100644 index 00000000..8f1a56d0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Processor/Markdown/MarkdownProcessorContext.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text; +using PSDocs.Configuration; +using PSDocs.Models; + +namespace PSDocs.Processor.Markdown +{ + internal sealed class MarkdownProcessorContext + { + private const char Space = ' '; + private const char Pipe = '|'; + private const char Hash = '#'; + private const string TripleBacktick = "```"; + private const string MARKDOWN_FRONTMATTER = "---"; + + public readonly IPSDocumentOption Option; + public readonly Document Document; + private readonly StringBuilder Builder; + + private LineEnding _Ending; + + internal MarkdownProcessorContext(IPSDocumentOption option, Document document) + { + Option = option; + Document = document; + Builder = new StringBuilder(); + _Ending = LineEnding.None; + } + + internal string GetString() + { + return Builder.Length > 0 ? Builder.ToString() : null; + } + + private enum LineEnding : byte + { + None = 0, + Normal = 1, + LineBreak = 2 + } + + internal void EndDocument() + { + Ending(); + if (_Ending == LineEnding.LineBreak) + Builder.Remove(Builder.Length - Environment.NewLine.Length, Environment.NewLine.Length); + } + + internal void LineBreak() + { + Ending(shouldBreak: true); + } + + internal void Ending(bool shouldBreak = false) + { + if (_Ending == LineEnding.LineBreak || (_Ending == LineEnding.Normal && !shouldBreak)) + return; + + if (shouldBreak && _Ending == LineEnding.None) + Builder.Append(Environment.NewLine); + + Builder.Append(Environment.NewLine); + _Ending = shouldBreak ? LineEnding.LineBreak : LineEnding.Normal; + } + + internal void WriteLine(params string[] line) + { + if (line == null || line.Length == 0) + return; + + for (var i = 0; i < line.Length; i++) + { + Write(line[i]); + } + Ending(); + } + + internal void Write(string text) + { + _Ending = LineEnding.None; + Builder.Append(text); + } + + internal void Write(char c, int count) + { + if (count == 0) + return; + + _Ending = LineEnding.None; + Builder.Append(c, count); + } + + internal void WriteSpace(int count = 1) + { + Write(Space, count); + } + + internal void WritePipe() + { + Write(Pipe, 1); + } + + internal void WriteTripleBacktick() + { + Write(TripleBacktick); + } + + internal void WriteHeaderHash(int count) + { + Write(Hash, count); + WriteSpace(); + } + + internal void WriteFrontMatter() + { + Write(MARKDOWN_FRONTMATTER); + Ending(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Properties/AssemblyInfo.cs b/packages/psdocs/src/PSDocs/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..b23f9a83 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Properties/AssemblyInfo.cs @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("PSDocs.Benchmark")] +[assembly: InternalsVisibleTo("PSDocs.Tests")] + diff --git a/packages/psdocs/src/PSDocs/Resources/PSDocsResources.Designer.cs b/packages/psdocs/src/PSDocs/Resources/PSDocsResources.Designer.cs new file mode 100644 index 00000000..aefa59c9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Resources/PSDocsResources.Designer.cs @@ -0,0 +1,261 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PSDocs.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class PSDocsResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal PSDocsResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSDocs.Resources.PSDocsResources", typeof(PSDocsResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Target failed Type precondition. + /// + internal static string DebugTargetTypeMismatch { + get { + return ResourceManager.GetString("DebugTargetTypeMismatch", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Could not find required parameter '{0}' for definition at {1}.. + /// + internal static string DefinitionParameterNotFound { + get { + return ResourceManager.GetString("DefinitionParameterNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][D] -- Found {0} PSDocs module(s). + /// + internal static string FoundModules { + get { + return ResourceManager.GetString("FoundModules", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The included file was not found.. + /// + internal static string IncludeNotFound { + get { + return ResourceManager.GetString("IncludeNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Document nesting was detected for definition at {0}. Definitions must not be nested.. + /// + internal static string InvalidDocumentNesting { + get { + return ResourceManager.GetString("InvalidDocumentNesting", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An invalid ErrorAction ({0}) was specified for definition at {1}. Ignore | Stop are supported.. + /// + internal static string InvalidErrorAction { + get { + return ResourceManager.GetString("InvalidErrorAction", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A culture must be specified when no culture is specified by the system. To specify a culture use the 'Output.Culture' option.. + /// + internal static string InvariantCulture { + get { + return ResourceManager.GetString("InvariantCulture", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A label must be set.. + /// + internal static string LabelNullOrEmpty { + get { + return ResourceManager.GetString("LabelNullOrEmpty", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Options file does not exist.. + /// + internal static string OptionsNotFound { + get { + return ResourceManager.GetString("OptionsNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Read JSON failed.. + /// + internal static string ReadJsonFailed { + get { + return ResourceManager.GetString("ReadJsonFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][D] -- Scanning for source files in module: {0}. + /// + internal static string ScanModule { + get { + return ResourceManager.GetString("ScanModule", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][D] -- Scanning for source files: {0}. + /// + internal static string ScanSource { + get { + return ResourceManager.GetString("ScanSource", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The script was not found.. + /// + internal static string ScriptNotFound { + get { + return ResourceManager.GetString("ScriptNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][S][Trace] -- {0}: {1} {0} {2}. + /// + internal static string SelectorExpressionTrace { + get { + return ResourceManager.GetString("SelectorExpressionTrace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][S][Trace] -- {0}. + /// + internal static string SelectorMatchTrace { + get { + return ResourceManager.GetString("SelectorMatchTrace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The selector '{0}' specified for definition at {1} could not be found.. + /// + internal static string SelectorNotFound { + get { + return ResourceManager.GetString("SelectorNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to [PSDocs][S][Trace] -- {0}: {1}. + /// + internal static string SelectorTrace { + get { + return ResourceManager.GetString("SelectorTrace", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Can not serialize a null PSObject.. + /// + internal static string SerializeNullPSObject { + get { + return ResourceManager.GetString("SerializeNullPSObject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Create path. + /// + internal static string ShouldCreatePath { + get { + return ResourceManager.GetString("ShouldCreatePath", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Write file. + /// + internal static string ShouldWriteFile { + get { + return ResourceManager.GetString("ShouldWriteFile", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The source was not found.. + /// + internal static string SourceNotFound { + get { + return ResourceManager.GetString("SourceNotFound", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The specified document title is empty and will be skipped.. + /// + internal static string TitleEmpty { + get { + return ResourceManager.GetString("TitleEmpty", resourceCulture); + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Resources/PSDocsResources.resx b/packages/psdocs/src/PSDocs/Resources/PSDocsResources.resx new file mode 100644 index 00000000..26c6627c --- /dev/null +++ b/packages/psdocs/src/PSDocs/Resources/PSDocsResources.resx @@ -0,0 +1,188 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Target failed Type precondition + + + Could not find required parameter '{0}' for definition at {1}. + + + [PSDocs][D] -- Found {0} PSDocs module(s) + + + The included file was not found. + + + Document nesting was detected for definition at {0}. Definitions must not be nested. + + + An invalid ErrorAction ({0}) was specified for definition at {1}. Ignore | Stop are supported. + + + A culture must be specified when no culture is specified by the system. To specify a culture use the 'Output.Culture' option. + + + A label must be set. + + + Options file does not exist. + Occurs when explicit path to a YAML file is specified and doesn't exist. + + + Read JSON failed. + + + [PSDocs][D] -- Scanning for source files in module: {0} + + + [PSDocs][D] -- Scanning for source files: {0} + + + The script was not found. + + + [PSDocs][S][Trace] -- {0}: {1} {0} {2} + + + [PSDocs][S][Trace] -- {0} + + + The selector '{0}' specified for definition at {1} could not be found. + + + [PSDocs][S][Trace] -- {0}: {1} + + + Can not serialize a null PSObject. + + + Create path + + + Write file + + + The source was not found. + Occurs when the source file specified does not exist. + + + The specified document title is empty and will be skipped. + + \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/Resources/ViewStrings.Designer.cs b/packages/psdocs/src/PSDocs/Resources/ViewStrings.Designer.cs new file mode 100644 index 00000000..27b72f93 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Resources/ViewStrings.Designer.cs @@ -0,0 +1,81 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace PSDocs.Resources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class ViewStrings { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal ViewStrings() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("PSDocs.Resources.ViewStrings", typeof(ViewStrings).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Module. + /// + public static string Module { + get { + return ResourceManager.GetString("Module", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Name. + /// + public static string Name { + get { + return ResourceManager.GetString("Name", resourceCulture); + } + } + } +} diff --git a/packages/psdocs/src/PSDocs/Resources/ViewStrings.resx b/packages/psdocs/src/PSDocs/Resources/ViewStrings.resx new file mode 100644 index 00000000..0302f19b --- /dev/null +++ b/packages/psdocs/src/PSDocs/Resources/ViewStrings.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Module + + + Name + + \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/Runtime/Configuration.cs b/packages/psdocs/src/PSDocs/Runtime/Configuration.cs new file mode 100644 index 00000000..12e6eaed --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/Configuration.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using System.Dynamic; + +namespace PSDocs.Runtime +{ + /// + /// A set of custom configuration values that are exposed at runtime. + /// + public sealed class Configuration : DynamicObject + { + private readonly RunspaceContext _Context; + + internal Configuration(RunspaceContext context) + { + _Context = context; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + result = null; + if (_Context == null || binder == null || string.IsNullOrEmpty(binder.Name)) + return false; + + // Get from configuration + return TryGetValue(binder.Name, out result); + } + + public string[] GetStringValues(string configurationKey) + { + if (!TryGetValue(configurationKey, out var value) || value == null) + return System.Array.Empty(); + + if (value is string valueT) + return new string[] { valueT }; + + if (value is string[] result) + return result; + + if (value is IEnumerable c) + { + var cList = new List(); + foreach (var v in c) + cList.Add(v.ToString()); + + return cList.ToArray(); + } + return new string[] { value.ToString() }; + } + + public object GetValueOrDefault(string configurationKey, object defaultValue) + { + if (!TryGetValue(configurationKey, out var value) || value == null) + return defaultValue; + + return value; + } + + public bool GetBoolOrDefault(string configurationKey, bool defaultValue) + { + if (!TryGetValue(configurationKey, out var value) || !TryBool(value, out var bvalue)) + return defaultValue; + + return bvalue; + } + + private bool TryGetValue(string name, out object value) + { + value = null; + if (_Context == null) + return false; + + return _Context.Pipeline.Option.Configuration.TryGetValue(name, out value); + } + + private bool TryBool(object o, out bool value) + { + value = default; + if (o is bool bvalue || (o is string svalue && bool.TryParse(svalue, out bvalue))) + { + value = bvalue; + return true; + } + return false; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/DocumentContext.cs b/packages/psdocs/src/PSDocs/Runtime/DocumentContext.cs new file mode 100644 index 00000000..0b9a26e9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/DocumentContext.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Specialized; + +namespace PSDocs.Runtime +{ + internal sealed class DocumentContext + { + internal DocumentContext(RunspaceContext runspaceContext) + { + Culture = runspaceContext?.Culture; + Metadata = new OrderedDictionary(); + Data = new Hashtable(); + } + + public string InstanceName { get; internal set; } + + public string Culture { get; } + + public string OutputPath { get; internal set; } + + public OrderedDictionary Metadata { get; } + + public Hashtable Data { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/Host.cs b/packages/psdocs/src/PSDocs/Runtime/Host.cs new file mode 100644 index 00000000..4b9827f0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/Host.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System; +using System.Globalization; +using System.Management.Automation.Host; + +namespace PSDocs.Runtime +{ + internal sealed class Host : PSHost + { + public override CultureInfo CurrentCulture => throw new NotImplementedException(); + + public override CultureInfo CurrentUICulture => throw new NotImplementedException(); + + public override Guid InstanceId => throw new NotImplementedException(); + + public override string Name => throw new NotImplementedException(); + + public override PSHostUserInterface UI => throw new NotImplementedException(); + + public override Version Version => throw new NotImplementedException(); + + public override void EnterNestedPrompt() + { + throw new NotImplementedException(); + } + + public override void ExitNestedPrompt() + { + throw new NotImplementedException(); + } + + public override void NotifyBeginApplication() + { + throw new NotImplementedException(); + } + + public override void NotifyEndApplication() + { + throw new NotImplementedException(); + } + + public override void SetShouldExit(int exitCode) + { + throw new NotImplementedException(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/HostHelper.cs b/packages/psdocs/src/PSDocs/Runtime/HostHelper.cs new file mode 100644 index 00000000..a463522e --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/HostHelper.cs @@ -0,0 +1,312 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using PSDocs.Annotations; +using PSDocs.Data; +using PSDocs.Data.Internal; +using PSDocs.Definitions; +using PSDocs.Definitions.Conventions; +using PSDocs.Definitions.Selectors; +using PSDocs.Pipeline; +using PSDocs.Resources; + +namespace PSDocs.Runtime +{ + internal static class HostHelper + { + /// + /// Executes get document builders from Document script blocks. + /// + internal static IDocumentBuilder[] GetDocumentBuilder(RunspaceContext context, Source[] source) + { + context.PushScope(RunspaceScope.Source); + var blocks = GetLanguageBlock(context, source); + var documents = ToDocument(blocks, context); + var conventions = GetConventions(blocks, context); + return ToDocumentBuilder(documents, conventions); + } + + internal static IDocumentDefinition[] GetDocumentBlock(RunspaceContext context, Source[] source) + { + return ToDocument(GetLanguageBlock(context, source), context); + } + + internal static void ImportResource(Source[] source, RunspaceContext context) + { + Import(ReadYamlObjects(source, context), context); + } + + /// + /// Read YAML objects and return selectors. + /// + internal static IEnumerable GetSelector(RunspaceContext context, Source[] source) + { + return ToSelectorV1(ReadYamlObjects(source, context), context); + } + + /// + /// Called from PowerShell to get additional metdata from a language block, such as comment help. + /// + /// + /// + /// + internal static CommentMetadata GetCommentMeta(string path, int lineNumber, int offset) + { + var context = RunspaceContext.CurrentThread; + //if (lineNumber < 0 || context.Pipeline.ExecutionScope == ExecutionScope.None || context.Source.SourceContentCache == null) + // return new CommentMetadata(); + + var lines = context.Source.Content; + var i = lineNumber; + var comments = new List(); + + // Back track lines with comments immediately before block + for (; i >= 0 && lines[i].Contains("#"); i--) + comments.Insert(0, lines[i]); + + // Check if any comments were found + var metadata = new CommentMetadata(); + if (comments.Count > 0) + { + foreach (var comment in comments) + { + if (comment.StartsWith("# Synopsis: ", StringComparison.OrdinalIgnoreCase)) + metadata.Synopsis = comment.Substring(12); + } + } + return metadata; + } + + /// + /// Execute one or more PowerShell script files to get language blocks. + /// + /// + /// + /// + private static ILanguageBlock[] GetLanguageBlock(RunspaceContext context, Source[] sources) + { + var results = new List(); + var ps = context.NewPowerShell(); + context.PushScope(RunspaceScope.Source); + try + { + // Process each source + foreach (var source in sources) + { + // Process search file per source + foreach (var file in source.File) + { + if (file.Type != SourceType.Script) + continue; + + ps.Commands.Clear(); + if (!context.EnterSourceFile(file)) + throw new FileNotFoundException(PSDocsResources.ScriptNotFound, file.Path); + + var scriptAst = System.Management.Automation.Language.Parser.ParseFile(file.Path, out var tokens, out var errors); + var visitor = new LanguageAst(context.Pipeline); + scriptAst.Visit(visitor); + + if (visitor.Errors != null && visitor.Errors.Count > 0) + { + foreach (var record in visitor.Errors) + context.Pipeline.Writer?.WriteError(record); + + continue; + } + if (errors != null && errors.Length > 0) + { + foreach (var error in errors) + context.Pipeline.Writer?.WriteError(error); + + continue; + } + + try + { + // Invoke script + ps.AddScript(string.Concat("& '", file.Path, "'"), true); + var invokeResults = ps.Invoke(); + + // Discovery has errors so skip this file + if (ps.HadErrors) + continue; + + foreach (var ir in invokeResults) + { + if (ir.BaseObject is ScriptDocumentBlock document) + results.Add(document); + + if (ir.BaseObject is ScriptBlockDocumentConvention convention) + results.Add(convention); + } + } + catch (Exception e) + { + context.WriteRuntimeException(sourceFile: file.Path, inner: e); + } + } + } + } + finally + { + context.ExitSourceFile(); + context.PopScope(); + ps.Runspace = null; + ps.Dispose(); + } + return results.ToArray(); + } + + private static IEnumerable ReadYamlObjects(Source[] sources, RunspaceContext context) + { + var builder = new ResourceBuilder(); + try + { + foreach (var source in sources) + { + foreach (var file in source.File) + { + if (file.Type != SourceType.Yaml) + continue; + + context.EnterSourceFile(file); + builder.FromFile(file); + } + } + } + finally + { + context.ExitSourceFile(); + } + return builder.Build(); + } + + private static void Import(IEnumerable blocks, RunspaceContext context) + { + foreach (var resource in blocks.OfType().ToArray()) + context.Pipeline.Import(resource); + } + + + /// + /// Convert document blocks to document builders. + /// + private static IDocumentBuilder[] ToDocumentBuilder(ScriptDocumentBlock[] documents, IDocumentConvention[] conventions) + { + var result = new ScriptDocumentBuilder[documents.Length]; + for (var i = 0; i < documents.Length; i++) + result[i] = new ScriptDocumentBuilder(documents[i], conventions); + + return result; + } + + /// + /// Convert language blocks to documents. + /// + private static ScriptDocumentBlock[] ToDocument(ILanguageBlock[] blocks, RunspaceContext context) + { + // Index by Id + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + foreach (var block in blocks.OfType()) + { + // Ignore blocks that don't match + if (!Match(context, block)) + continue; + + if (!results.ContainsKey(block.Id)) + results[block.Id] = block; + } + } + finally + { + //context.ExitSourceFile(); + } + return results.Values.ToArray(); + } + + private static SelectorV1[] ToSelectorV1(IEnumerable blocks, RunspaceContext context) + { + if (blocks == null) + return Array.Empty(); + + // Index selectors by Id + var results = new Dictionary(StringComparer.OrdinalIgnoreCase); + try + { + foreach (var block in blocks.OfType().ToArray()) + { + // Ignore selectors that don't match + if (!Match(context, block)) + continue; + + if (!results.ContainsKey(block.Id)) + results[block.Id] = block; + } + } + finally + { + context.ExitSourceFile(); + } + return results.Values.ToArray(); + } + + /// + /// Get conventions. + /// + private static IDocumentConvention[] GetConventions(ILanguageBlock[] blocks, RunspaceContext runspace) + { + // Index by Id + var index = new HashSet(StringComparer.OrdinalIgnoreCase); + var results = new List(blocks.Length); + try + { + foreach (var block in blocks.OfType()) + { + // Ignore blocks that don't match + if (!Match(runspace, block, out var order)) + continue; + + if (!index.Contains(block.Id)) + results.Insert(order, block); + } + } + finally + { + //context.ExitSourceFile(); + } + results.Insert(0, new DefaultDocumentConvention("default")); + return results.ToArray(); + } + + private static bool Match(RunspaceContext context, ScriptDocumentBlock block) + { + return context.Pipeline.Filter.Match(block.Name, block.Tag); + } + + private static bool Match(RunspaceContext runspace, ScriptBlockDocumentConvention block, out int order) + { + order = int.MaxValue; + for (var i = 0; runspace.Pipeline.Convention != null && i < runspace.Pipeline.Convention.Length; i++) + { + if (StringComparer.OrdinalIgnoreCase.Equals(runspace.Pipeline.Convention[i], block.Name) || StringComparer.OrdinalIgnoreCase.Equals(runspace.Pipeline.Convention[i], block.Id)) + { + order = i; + return true; + } + } + return false; + } + + private static bool Match(RunspaceContext context, SelectorV1 resource) + { + return true; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/HostState.cs b/packages/psdocs/src/PSDocs/Runtime/HostState.cs new file mode 100644 index 00000000..45d553d9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/HostState.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Management.Automation; +using System.Management.Automation.Runspaces; +using PSDocs.Commands; + +namespace PSDocs.Runtime +{ + internal static class HostState + { + public sealed class PSDocsVariable : PSVariable + { + internal const string VariableName = "PSDocs"; + + private readonly PSDocs _Value; + + public PSDocsVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) + { + _Value = new PSDocs(); + } + + public override object Value => _Value; + } + + public sealed class LocalizedDataVariable : PSVariable + { + internal const string VariableName = "LocalizedData"; + + private readonly LocalizedData _Value; + + public LocalizedDataVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) + { + _Value = new LocalizedData(); + } + + internal LocalizedDataVariable(RunspaceContext context) + : this() + { + _Value = new LocalizedData(context); + } + + public override object Value => _Value; + } + + public sealed class InstanceNameVariable : PSVariable + { + internal const string VariableName = "InstanceName"; + + public InstanceNameVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) { } + + public override object Value => RunspaceContext.CurrentThread?.DocumentContext.InstanceName ?? RunspaceContext.CurrentThread.Builder.Name; + } + + public sealed class TargetObjectVariable : PSVariable + { + internal const string VariableName = "TargetObject"; + + public TargetObjectVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) { } + + public override object Value => RunspaceContext.CurrentThread.TargetObject.Value; + } + + public sealed class InputObjectVariable : PSVariable + { + internal const string VariableName = "InputObject"; + + public InputObjectVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) { } + + public override object Value => RunspaceContext.CurrentThread.TargetObject.Value; + } + + public sealed class DocumentVariable : PSVariable + { + internal const string VariableName = "Document"; + + public DocumentVariable() + : base(VariableName, null, ScopedItemOptions.ReadOnly) { } + + public override object Value => RunspaceContext.CurrentThread.Builder.Document; + } + + /// + /// Define language commands. + /// + private static readonly SessionStateCmdletEntry[] BuiltInCmdlets = new SessionStateCmdletEntry[] + { + new SessionStateCmdletEntry(LanguageCmdlets.NewDefinition, typeof(DefinitionCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.ExportConvention, typeof(ExportConventionCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.NewSection, typeof(SectionCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.InvokeBlock, typeof(InvokeDocumentCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.InvokeConvention, typeof(InvokeConventionCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatBlockQuote, typeof(BlockQuoteCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatCode, typeof(CodeCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatList, typeof(ListCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatNote, typeof(NoteCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatTable, typeof(TableCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.FormatWarning, typeof(WarningCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.SetMetadata, typeof(MetadataCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.SetTitle, typeof(TitleCommand), null), + new SessionStateCmdletEntry(LanguageCmdlets.AddInclude, typeof(IncludeCommand), null), + }; + + /// + /// Define language aliases. + /// + private static readonly SessionStateAliasEntry[] BuiltInAliases = new SessionStateAliasEntry[] + { + new SessionStateAliasEntry(LanguageKeywords.Document, LanguageCmdlets.NewDefinition, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Section, LanguageCmdlets.NewSection, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.BlockQuote, LanguageCmdlets.FormatBlockQuote, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Code, LanguageCmdlets.FormatCode, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.List, LanguageCmdlets.FormatList, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Note, LanguageCmdlets.FormatNote, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Table, LanguageCmdlets.FormatTable, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Warning, LanguageCmdlets.FormatWarning, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Metadata, LanguageCmdlets.SetMetadata, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Title, LanguageCmdlets.SetTitle, string.Empty, ScopedItemOptions.ReadOnly), + new SessionStateAliasEntry(LanguageKeywords.Include, LanguageCmdlets.AddInclude, string.Empty, ScopedItemOptions.ReadOnly), + }; + + public static InitialSessionState CreateSessionState() + { + var state = InitialSessionState.CreateDefault(); + state.ThreadOptions = PSThreadOptions.UseCurrentThread; + state.ThrowOnRunspaceOpenError = true; + RemoveDefault(state); + + // Add in language elements + state.Commands.Add(BuiltInCmdlets); + state.Commands.Add(BuiltInAliases); + + // Set execution policy + SetExecutionPolicy(state, executionPolicy: Microsoft.PowerShell.ExecutionPolicy.RemoteSigned); + return state; + } + + private static void RemoveDefault(InitialSessionState state) + { + if (state.Commands[LanguageCmdlets.FormatTable].Count > 0) + state.Commands.Remove(LanguageCmdlets.FormatTable, null); + + if (state.Commands[LanguageCmdlets.FormatList].Count > 0) + state.Commands.Remove(LanguageCmdlets.FormatList, null); + } + + private static bool IsReplacedCommand(string name) + { + return name == LanguageCmdlets.FormatTable || name == LanguageCmdlets.FormatList; + } + + private static void SetExecutionPolicy(InitialSessionState state, Microsoft.PowerShell.ExecutionPolicy executionPolicy) + { + // Only set execution policy on Windows + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + state.ExecutionPolicy = executionPolicy; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/ILanguageBlock.cs b/packages/psdocs/src/PSDocs/Runtime/ILanguageBlock.cs new file mode 100644 index 00000000..d7389e2d --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/ILanguageBlock.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Runtime +{ + public interface ILanguageBlock + { + string Id { get; } + + string SourcePath { get; } + + string Module { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/LanguageAst.cs b/packages/psdocs/src/PSDocs/Runtime/LanguageAst.cs new file mode 100644 index 00000000..6820ff67 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/LanguageAst.cs @@ -0,0 +1,230 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Threading; +using PSDocs.Definitions; +using PSDocs.Pipeline; +using PSDocs.Resources; + +namespace PSDocs.Runtime +{ + internal sealed class LanguageAst : AstVisitor + { + private const string PARAMETER_NAME = "Name"; + private const string PARAMETER_BODY = "Body"; + private const string PARAMETER_ERRORACTION = "ErrorAction"; + private const string PARAMETER_WITH = "With"; + private const string DOCUMENT_KEYWORD = "Document"; + private const string ERRORID_PARAMETERNOTFOUND = "PSDocs.Parse.DefinitionParameterNotFound"; + private const string ERRORID_INVALIDDOCUMENTNESTING = "PSDocs.Parse.InvalidDocumentNesting"; + private const string ERRORID_INVALIDERRORACTION = "PSDocs.Parse.InvalidErrorAction"; + private const string ERRORID_SELECTORNOTFOUND = "PSDocs.Parse.SelectorNotFound"; + + private readonly PipelineContext _Context; + private readonly StringComparer _Comparer; + + internal List Errors; + + internal LanguageAst(Pipeline.PipelineContext context) + { + _Context = context; + _Comparer = StringComparer.OrdinalIgnoreCase; + } + + private sealed class ParameterBindResult + { + public ParameterBindResult() + { + Bound = new Dictionary(StringComparer.OrdinalIgnoreCase); + Unbound = new List(); + _Offset = 0; + } + + public Dictionary Bound; + public List Unbound; + + private int _Offset; + + public bool Has(string parameterName, out TAst parameterValue) where TAst : CommandElementAst + { + var result = Bound.TryGetValue(parameterName, out var value) && value is TAst; + parameterValue = result ? value as TAst : null; + return result; + } + + public bool Has(string parameterName, int position, out TAst value) where TAst : CommandElementAst + { + // Try bound + if (Has(parameterName, out value)) + { + _Offset++; + return true; + } + var relative = position - _Offset; + var result = Unbound.Count > relative && Unbound[relative] is TAst; + value = result ? Unbound[relative] as TAst : null; + return result; + } + } + + public override AstVisitAction VisitCommand(CommandAst commandAst) + { + if (IsDefinition(commandAst)) + { + var valid = NotNested(commandAst) && + HasValidErrorAction(commandAst) && + HasRequiredParameters(commandAst) && + HasValidSelector(commandAst); + + return valid ? base.VisitCommand(commandAst) : AstVisitAction.SkipChildren; + } + return base.VisitCommand(commandAst); + } + + /// + /// Determines if the definition has a Body parameter. + /// + private bool HasBodyParameter(CommandAst commandAst, ParameterBindResult bindResult) + { + if (bindResult.Has(PARAMETER_BODY, 1, out ScriptBlockExpressionAst _)) + return true; + + ReportError(ERRORID_PARAMETERNOTFOUND, PSDocsResources.DefinitionParameterNotFound, PARAMETER_BODY, ReportExtent(commandAst.Extent)); + return false; + } + + /// + /// Determines if the definition has a Name parameter. + /// + private bool HasNameParameter(CommandAst commandAst, ParameterBindResult bindResult) + { + if (bindResult.Has(PARAMETER_NAME, 0, out StringConstantExpressionAst value) && !string.IsNullOrEmpty(value.Value)) + return true; + + ReportError(ERRORID_PARAMETERNOTFOUND, PSDocsResources.DefinitionParameterNotFound, PARAMETER_NAME, ReportExtent(commandAst.Extent)); + return false; + } + + /// + /// Determines if the definition is nested in another definition. + /// + private bool NotNested(CommandAst commandAst) + { + if (GetParentBlock(commandAst)?.Parent == null) + return true; + + ReportError(ERRORID_INVALIDDOCUMENTNESTING, PSDocsResources.InvalidDocumentNesting, ReportExtent(commandAst.Extent)); + return false; + } + + /// + /// Determines if the definition has required parameters. + /// + private bool HasRequiredParameters(CommandAst commandAst) + { + var bindResult = BindParameters(commandAst); + return HasNameParameter(commandAst, bindResult) && HasBodyParameter(commandAst, bindResult); + } + + /// + /// Determine if the definition has allowed ErrorAction options. + /// + private bool HasValidErrorAction(CommandAst commandAst) + { + var bindResult = BindParameters(commandAst); + if (!bindResult.Has(PARAMETER_ERRORACTION, 0, out StringConstantExpressionAst value)) + return true; + + if (!Enum.TryParse(value.Value, out ActionPreference result) || (result == ActionPreference.Ignore || result == ActionPreference.Stop)) + return true; + + ReportError(ERRORID_INVALIDERRORACTION, PSDocsResources.InvalidErrorAction, value.Value, ReportExtent(commandAst.Extent)); + return false; + } + + private bool HasValidSelector(CommandAst commandAst) + { + var bindResult = BindParameters(commandAst); + if (!bindResult.Has(PARAMETER_WITH, out StringConstantExpressionAst value)) + return true; + + var selectorId = ResourceHelper.GetId(RunspaceContext.CurrentThread.Source.File.ModuleName, value.Value); + if (_Context.Selector.ContainsKey(selectorId)) + return true; + + ReportError(ERRORID_SELECTORNOTFOUND, PSDocsResources.SelectorNotFound, value.Value, ReportExtent(commandAst.Extent)); + return false; + } + + /// + /// Determines if the command is a document definition. + /// + private bool IsDefinition(CommandAst commandAst) + { + return _Comparer.Equals(commandAst.GetCommandName(), DOCUMENT_KEYWORD); + } + + private static ParameterBindResult BindParameters(CommandAst commandAst) + { + var result = new ParameterBindResult(); + var i = 1; + var next = 2; + for (; i < commandAst.CommandElements.Count; i++, next++) + { + // Is named parameter + if (commandAst.CommandElements[i] is CommandParameterAst parameter && next < commandAst.CommandElements.Count) + { + result.Bound.Add(parameter.ParameterName, commandAst.CommandElements[next]); + i++; + next++; + } + else + { + result.Unbound.Add(commandAst.CommandElements[i]); + } + } + return result; + } + + private void ReportError(string errorId, string message, params object[] args) + { + ReportError(new global::PSDocs.Pipeline.ParseException( + message: string.Format(Thread.CurrentThread.CurrentCulture, message, args), + errorId: errorId + )); + } + + private void ReportError(global::PSDocs.Pipeline.ParseException exception) + { + if (Errors == null) + { + Errors = new List(); + } + + Errors.Add(new ErrorRecord( + exception: exception, + errorId: exception.ErrorId, + errorCategory: ErrorCategory.InvalidOperation, + targetObject: null + )); + } + + private static string ReportExtent(IScriptExtent extent) + { + return string.Concat(extent.File, " line ", extent.StartLineNumber); + } + + private static ScriptBlockAst GetParentBlock(Ast ast) + { + var block = ast; + while (block != null && block is not ScriptBlockAst) + block = block.Parent; + + return (ScriptBlockAst)block; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/LanguageScriptBlock.cs b/packages/psdocs/src/PSDocs/Runtime/LanguageScriptBlock.cs new file mode 100644 index 00000000..15a2f84a --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/LanguageScriptBlock.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSDocs.Runtime +{ + internal sealed class LanguageScriptBlock + { + private readonly PowerShell _Block; + + public LanguageScriptBlock(PowerShell block) + { + _Block = block; + } + + public void Invoke() + { + _Block.Invoke(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/LocalizedData.cs b/packages/psdocs/src/PSDocs/Runtime/LocalizedData.cs new file mode 100644 index 00000000..79de5898 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/LocalizedData.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Dynamic; + +namespace PSDocs.Runtime +{ + public sealed class LocalizedData : DynamicObject + { + private readonly RunspaceContext _Context; + + public LocalizedData() { } + + internal LocalizedData(RunspaceContext context) + { + _Context = context; + } + + public override bool TryGetMember(GetMemberBinder binder, out object result) + { + var hashtable = GetContext().GetLocalizedStrings(); + if (hashtable.Count > 0 && binder != null && !string.IsNullOrEmpty(binder.Name) && hashtable.ContainsKey(binder.Name)) + { + result = hashtable[binder.Name]; + return true; + } + result = null; + return false; + } + + private RunspaceContext GetContext() + { + if (_Context == null) + return RunspaceContext.CurrentThread; + + return _Context; + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/PSDocs.cs b/packages/psdocs/src/PSDocs/Runtime/PSDocs.cs new file mode 100644 index 00000000..e301dbe1 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/PSDocs.cs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Management.Automation; +using System.Threading; + +namespace PSDocs.Runtime +{ + /// + /// A set of context properties that are exposed at runtime through the $PSDocs variable. + /// + public sealed class PSDocs : ScopedItem + { + private Configuration _Configuration; + private PSDocsDocument _Document; + private PSDocsSource _Source; + + public PSDocs() { } + + internal PSDocs(RunspaceContext context) + : base(context) { } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Exposed as helper for PowerShell.")] + public sealed class PSDocsDocument : ScopedItem + { + internal PSDocsDocument(RunspaceContext context) + : base(context) { } + + public string InstanceName + { + get + { + RequireScope(RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess | RunspaceScope.Condition | RunspaceScope.Document); + return GetContext().DocumentContext.InstanceName; + } + set + { + RequireScope(RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess); + if (string.IsNullOrEmpty(value)) + return; + + GetContext().DocumentContext.InstanceName = value; + } + } + + public string OutputPath + { + get + { + RequireScope(RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess | RunspaceScope.Condition | RunspaceScope.Document); + return GetContext().DocumentContext.OutputPath; + } + set + { + RequireScope(RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess); + if (string.IsNullOrEmpty(value)) + return; + + GetContext().DocumentContext.OutputPath = value; + } + } + + /// + /// Custom data for this document. + /// + public Hashtable Data + { + get + { + RequireScope(RunspaceScope.Document | RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess); + return GetContext().DocumentContext.Data; + } + } + } + + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1034:Nested types should not be visible", Justification = "Exposed as helper for PowerShell.")] + public sealed class PSDocsSource : ScopedItem + { + internal PSDocsSource(RunspaceContext context) + : base(context) { } + + public string Path => GetContext().TargetObject?.Source.Path; + + public string FullName => GetContext().TargetObject?.Source.FullName; + + public string DirectoryName => GetContext().TargetObject?.Source.DirectoryName; + } + + /// + /// The current target object. + /// + public PSObject TargetObject + { + get + { + RequireScope(RunspaceScope.Document | RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess | RunspaceScope.Condition); + return GetContext().TargetObject.Value; + } + } + + //public string Generator + //{ + // get + // { + // return GetContext().Generator; + // } + //} + + /// + /// Custom configuration values. + /// + public Configuration Configuration => GetConfiguration(); + + /// + /// The current culture. + /// + public string Culture + { + get + { + RequireScope(RunspaceScope.Document); + return GetContext().DocumentContext.Culture; + } + } + + public PSDocsDocument Document => GetDocument(); + + public IEnumerable Output + { + get + { + RequireScope(RunspaceScope.ConventionEnd); + return GetContext().Output; + } + } + + public PSDocsSource Source => GetSource(); + + /// + /// Format a string with arguments. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "Exposed as instance method for PowerShell.")] + public string Format(string value, params object[] args) + { + if (string.IsNullOrEmpty(value)) + return string.Empty; + + if (args == null || args.Length == 0) + return value; + else + return string.Format(Thread.CurrentThread.CurrentCulture, value, args); + } + + #region Helper methods + + private Configuration GetConfiguration() + { + RequireScope(RunspaceScope.All); + if (_Configuration == null) + _Configuration = new Configuration(GetContext()); + + return _Configuration; + } + + private PSDocsDocument GetDocument() + { + RequireScope(RunspaceScope.Runtime); + if (_Document == null) + _Document = new PSDocsDocument(GetContext()); + + return _Document; + } + + private PSDocsSource GetSource() + { + RequireScope(RunspaceScope.Document | RunspaceScope.ConventionBegin | RunspaceScope.ConventionProcess | RunspaceScope.Condition); + if (_Source == null) + _Source = new PSDocsSource(GetContext()); + + return _Source; + } + + #endregion Helper methods + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/ResourceExtent.cs b/packages/psdocs/src/PSDocs/Runtime/ResourceExtent.cs new file mode 100644 index 00000000..ac58dfa1 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/ResourceExtent.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Runtime +{ + internal interface IResourceExtent + { + string File { get; } + + int StartLineNumber { get; } + } + + internal sealed class ResourceExtent : IResourceExtent + { + internal ResourceExtent(string file, int startLineNumber) + { + File = file; + StartLineNumber = startLineNumber; + } + + public string File { get; } + + public int StartLineNumber { get; } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/RunspaceContext.cs b/packages/psdocs/src/PSDocs/Runtime/RunspaceContext.cs new file mode 100644 index 00000000..dba079f4 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/RunspaceContext.cs @@ -0,0 +1,435 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Management.Automation; +using System.Management.Automation.Language; +using System.Management.Automation.Runspaces; +using PSDocs.Configuration; +using PSDocs.Data.Internal; +using PSDocs.Definitions; +using PSDocs.Pipeline; + +namespace PSDocs.Runtime +{ + internal enum RunspaceScope + { + None = 0, + + Source = 1, + + Document = 2, + + Condition = 4, + + ConventionBegin = 8, + ConventionProcess = 16, + ConventionEnd = 32, + + Convention = ConventionBegin | ConventionProcess | ConventionEnd, + Runtime = Document | Condition | Convention, + All = Source | Document | Condition | Convention, + } + + /// + /// A context for a runspace. + /// + internal sealed class RunspaceContext : IDisposable + { + private const string ErrorPreference = "ErrorActionPreference"; + private const string WarningPreference = "WarningPreference"; + private const string VerbosePreference = "VerbosePreference"; + private const string DebugPreference = "DebugPreference"; + + [ThreadStatic] + internal static RunspaceContext CurrentThread; + + internal readonly Dictionary ExpressionCache; + + private readonly Dictionary _LocalizedDataCache; + + private Runspace _Runspace; + private string[] _Culture; + private readonly Stack _Scope; + + // Track whether Dispose has been called. + private bool _Disposed; + + public RunspaceContext(PipelineContext pipeline) + { + CurrentThread = this; + _Scope = new Stack(); + Pipeline = pipeline; + _Runspace = GetRunspace(); + _LocalizedDataCache = new Dictionary(); + ExpressionCache = new Dictionary(); + } + + public PipelineContext Pipeline { get; } + + public SourceScope Source { get; private set; } + + public ScriptDocumentBuilder Builder { get; private set; } + + public TargetObject TargetObject { get; private set; } + + public IEnumerable Output { get; private set; } + + public string Culture => _Culture[0]; + + public DocumentContext DocumentContext { get; private set; } + + internal void EnterDocument(string instanceName) + { + DocumentContext = new DocumentContext(this) + { + InstanceName = instanceName + }; + PushScope(RunspaceScope.Document); + } + + internal void ExitDocument() + { + DocumentContext = null; + PopScope(); + } + + internal void SetOutput(IEnumerable output) + { + Output = output; + } + + internal void ClearOutput() + { + Output = null; + } + + internal bool IsScope(RunspaceScope scope) + { + var current = _Scope.Peek(); + return (current & scope) == current; + } + + internal void PushScope(RunspaceScope scope) + { + _Scope.Push(scope); + } + + internal void PopScope() + { + _Scope.Pop(); + } + + internal PowerShell NewPowerShell() + { + CurrentThread = this; + var runspace = GetRunspace(); + var ps = PowerShell.Create(); + ps.Runspace = runspace; + EnableLogging(ps); + return ps; + } + + private Runspace GetRunspace() + { + if (_Runspace == null) + { + // Get session state + var state = HostState.CreateSessionState(); + state.LanguageMode = Pipeline.LanguageMode == LanguageMode.FullLanguage ? PSLanguageMode.FullLanguage : PSLanguageMode.ConstrainedLanguage; + + _Runspace = RunspaceFactory.CreateRunspace(state); + + if (Runspace.DefaultRunspace == null) + Runspace.DefaultRunspace = _Runspace; + + _Runspace.Open(); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.PSDocsVariable()); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.LocalizedDataVariable(this)); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.InstanceNameVariable()); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.TargetObjectVariable()); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.InputObjectVariable()); + _Runspace.SessionStateProxy.PSVariable.Set(new HostState.DocumentVariable()); + _Runspace.SessionStateProxy.PSVariable.Set(ErrorPreference, ActionPreference.Continue); + _Runspace.SessionStateProxy.PSVariable.Set(WarningPreference, ActionPreference.Continue); + _Runspace.SessionStateProxy.PSVariable.Set(VerbosePreference, ActionPreference.Continue); + _Runspace.SessionStateProxy.PSVariable.Set(DebugPreference, ActionPreference.Continue); + _Runspace.SessionStateProxy.Path.SetLocation(PSDocumentOption.GetWorkingPath()); + } + return _Runspace; + } + + #region SourceFile + + public bool EnterSourceFile(SourceFile file) + { + if (file == null || !File.Exists(file.Path)) + return false; + + Source = new SourceScope(file); + return true; + } + + public void ExitSourceFile() + { + Source = null; + } + + #endregion SourceFile + + #region Builder + + public void EnterBuilder(ScriptDocumentBuilder builder) + { + CurrentThread = this; + Builder = builder; + Pipeline.Option.SwitchScope(builder.Module); + } + + public void ExitBuilder() + { + Builder = null; + } + + #endregion Builder + + #region TargetObject + + public void EnterTargetObject(TargetObject targetObject) + { + TargetObject = targetObject; + } + + public void ExitTargetObject() + { + TargetObject = null; + } + + public bool TrySelector(string name) + { + name = ResourceHelper.GetId(Source.File.ModuleName, name); + if (TargetObject == null || Pipeline == null || !Pipeline.Selector.TryGetValue(name, out var selector)) + return false; + + var annotation = TargetObject.GetAnnotation(); + if (annotation.TryGetSelectorResult(selector, out var result)) + return result; + + result = selector.Match(TargetObject.Value); + annotation.SetSelectorResult(selector, result); + return result; + } + + #endregion TargetObject + + #region Culture + + public void EnterCulture(string culture) + { + _Culture = GetCultures(culture); + } + + /// + /// Build a list of cultures. + /// + private static string[] GetCultures(string culture) + { + var cultures = new List(); + if (!string.IsNullOrEmpty(culture)) + { + var c = new CultureInfo(culture); + while (c != null && !string.IsNullOrEmpty(c.Name)) + { + cultures.Add(c.Name); + c = c.Parent; + } + } + return cultures.ToArray(); + } + + private const string DATA_FILENAME = "PSDocs-strings.psd1"; + + private static readonly Hashtable Empty = new(); + + internal Hashtable GetLocalizedStrings() + { + var path = GetLocalizedPaths(DATA_FILENAME); + if (path == null || path.Length == 0) + return Empty; + + if (_LocalizedDataCache.TryGetValue(path[0], out var result)) + return result; + + result = ReadLocalizedStrings(path[0]) ?? new Hashtable(); + for (var i = 1; i < path.Length; i++) + result.AddUnique(ReadLocalizedStrings(path[i])); + + _LocalizedDataCache[path[0]] = result; + return result; + } + + private static Hashtable ReadLocalizedStrings(string path) + { + var ast = Parser.ParseFile(path, out var tokens, out var errors); + var data = ast.Find(a => a is HashtableAst, false); + if (data != null) + { + var result = (Hashtable)data.SafeGetValue(); + return result; + } + return null; + } + + public string GetLocalizedPath(string file) + { + if (string.IsNullOrEmpty(Source.File.ResourcePath)) + return null; + + for (var i = 0; i < _Culture.Length; i++) + { + if (TryLocalizedPath(_Culture[i], file, out var path)) + return path; + } + return null; + } + + public string[] GetLocalizedPaths(string file) + { + if (string.IsNullOrEmpty(Source.File.ResourcePath)) + return null; + + var result = new List(); + for (var i = 0; i < _Culture.Length; i++) + { + if (TryLocalizedPath(_Culture[i], file, out var path)) + result.Add(path); + } + return result.ToArray(); + } + + private bool TryLocalizedPath(string culture, string file, out string path) + { + path = null; + if (Source == null || string.IsNullOrEmpty(Source.File.ResourcePath)) + return false; + + path = Path.Combine(Source.File.ResourcePath, culture, file); + return File.Exists(path); + } + + #endregion Culture + + #region Logging + + private static void EnableLogging(PowerShell ps) + { + ps.Streams.Error.DataAdded += Error_DataAdded; + ps.Streams.Warning.DataAdded += Warning_DataAdded; + ps.Streams.Verbose.DataAdded += Verbose_DataAdded; + ps.Streams.Information.DataAdded += Information_DataAdded; + ps.Streams.Debug.DataAdded += Debug_DataAdded; + } + + internal void WriteRuntimeException(string sourceFile, Exception inner) + { + if (Pipeline == null || Pipeline.Writer == null) + return; + + var record = new ErrorRecord(new Pipeline.RuntimeException(sourceFile: sourceFile, innerException: inner), "PSDocs.Pipeline.RuntimeException", ErrorCategory.InvalidOperation, null); + Pipeline.Writer.WriteError(record); + } + + internal static void ThrowRuntimeException(string sourceFile, Exception inner) + { + throw new Pipeline.RuntimeException(sourceFile: sourceFile, innerException: inner); + } + + private static void Debug_DataAdded(object sender, DataAddedEventArgs e) + { + if (CurrentThread.Pipeline == null || CurrentThread.Pipeline.Writer == null) + return; + + var collection = sender as PSDataCollection; + var record = collection[e.Index]; + //CurrentThread._Logger.WriteDebug(debugRecord: record); + } + + private static void Information_DataAdded(object sender, DataAddedEventArgs e) + { + if (CurrentThread.Pipeline == null || CurrentThread.Pipeline.Writer == null) + return; + + var collection = sender as PSDataCollection; + var record = collection[e.Index]; + //CurrentThread._Logger.WriteInformation(informationRecord: record); + } + + private static void Verbose_DataAdded(object sender, DataAddedEventArgs e) + { + if (CurrentThread.Pipeline == null || CurrentThread.Pipeline.Writer == null) + return; + + var collection = sender as PSDataCollection; + var record = collection[e.Index]; + CurrentThread.Pipeline.Writer.WriteVerbose(record.Message); + } + + private static void Warning_DataAdded(object sender, DataAddedEventArgs e) + { + if (CurrentThread.Pipeline == null || CurrentThread.Pipeline.Writer == null) + return; + + var collection = sender as PSDataCollection; + var record = collection[e.Index]; + CurrentThread.Pipeline.Writer.WriteWarning(record.Message); + } + + private static void Error_DataAdded(object sender, DataAddedEventArgs e) + { + if (CurrentThread.Pipeline == null || CurrentThread.Pipeline.Writer == null) + return; + + var collection = sender as PSDataCollection; + var record = collection[e.Index]; + CurrentThread.Pipeline.Writer.WriteError(record); + } + + #endregion Logging + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + if (Builder != null) + { + Builder.Dispose(); + Builder = null; + } + if (_Runspace != null) + { + _Runspace.Dispose(); + _Runspace = null; + } + CurrentThread = null; + } + _Disposed = true; + } + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/ScopedItem.cs b/packages/psdocs/src/PSDocs/Runtime/ScopedItem.cs new file mode 100644 index 00000000..b999dde9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/ScopedItem.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Pipeline; + +namespace PSDocs.Runtime +{ + public abstract class ScopedItem + { + private readonly RunspaceContext _Context; + + internal ScopedItem() + { + + } + + internal ScopedItem(RunspaceContext context) + { + _Context = context; + } + + #region Helper methods + + internal void RequireScope(RunspaceScope scope) + { + if (GetContext().IsScope(scope)) + return; + + throw new RuntimeException(); + } + + internal RunspaceContext GetContext() + { + if (_Context == null) + return RunspaceContext.CurrentThread; + + return _Context; + } + + #endregion Helper methods + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/ScriptDocumentBlock.cs b/packages/psdocs/src/PSDocs/Runtime/ScriptDocumentBlock.cs new file mode 100644 index 00000000..638443b9 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/ScriptDocumentBlock.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Management.Automation; +using PSDocs.Definitions; +using PSDocs.Pipeline; + +namespace PSDocs.Runtime +{ + /// + /// A Document block. + /// + [DebuggerDisplay("{Id} @{SourcePath}")] + internal sealed class ScriptDocumentBlock : IDocumentDefinition, IDisposable + { + internal readonly PowerShell Body; + internal readonly string[] Tag; + + // Track whether Dispose has been called. + private bool _Disposed; + + internal ScriptDocumentBlock(SourceFile source, string name, PowerShell body, string[] tag, IResourceExtent extent) + { + Source = source; + Name = name; + Id = ResourceHelper.GetId(source.ModuleName, name); + Body = body; + Tag = tag; + Extent = extent; + } + + public string Id { get; } + + public string Name { get; } + + public string SourcePath => Source.Path; + + public string Module => Source.ModuleName; + + internal SourceFile Source { get; } + + internal IResourceExtent Extent { get; } + + #region IDisposable + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + private void Dispose(bool disposing) + { + if (!_Disposed) + { + if (disposing) + { + Body.Dispose(); + } + _Disposed = true; + } + } + + #endregion IDisposable + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/SourceScope.cs b/packages/psdocs/src/PSDocs/Runtime/SourceScope.cs new file mode 100644 index 00000000..ea4b12cf --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/SourceScope.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using PSDocs.Pipeline; + +namespace PSDocs.Runtime +{ + internal sealed class SourceScope + { + public readonly SourceFile File; + public readonly string[] Content; + + public SourceScope(SourceFile source) + { + File = source; + Content = System.IO.File.ReadAllLines(source.Path, Encoding.UTF8); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/StringContent.cs b/packages/psdocs/src/PSDocs/Runtime/StringContent.cs new file mode 100644 index 00000000..3c044972 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/StringContent.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace PSDocs.Runtime +{ + internal sealed class StringContent + { + private readonly string _Input; + + public StringContent(string input, string info = null) + { + _Input = input; + Info = info; + } + + public string Info { get; } + + internal string[] ReadLines() + { + if (string.IsNullOrEmpty(_Input)) + return Array.Empty(); + + var lines = new List(); + var stream = new StringStream(_Input); + stream.SkipLineEnding(); + var indent = stream.GetIndent(); + + while (!stream.EOF) + { + stream.SkipIndent(indent); + if (stream.UntilLineEnding(out var text)) + lines.Add(text); + + stream.SkipLineEnding(); + } + return lines.ToArray(); + } + } +} diff --git a/packages/psdocs/src/PSDocs/Runtime/StringStream.cs b/packages/psdocs/src/PSDocs/Runtime/StringStream.cs new file mode 100644 index 00000000..2f61da07 --- /dev/null +++ b/packages/psdocs/src/PSDocs/Runtime/StringStream.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Runtime +{ + internal sealed class StringStream + { + private const char EmptyChar = '\0'; + private const char LF = '\n'; + private const char CR = '\r'; + + private readonly string _Input; + private readonly int _LastIndex; + + private int _Position = -1; + private char _Current = EmptyChar; + + public bool EOF => _Position > _LastIndex; + + public StringStream(string input) + { + _Input = input; + _LastIndex = input.Length - 1; + Next(); + } + + internal void SkipLineEnding() + { + if (!IsLineEnding(_Current)) + return; + + if (_Current == CR) // \r + Next(); + + if (_Current == LF) // \n + Next(); + } + + public bool UntilLineEnding(out string text) + { + text = string.Empty; + if (EOF) + return false; + + var count = 0; + var startingPos = _Position; + while (!EOF && !IsLineEnding(_Current)) + { + count++; + Next(); + } + text = _Input.Substring(startingPos, count); + return true; + } + + public int GetIndent() + { + var offset = 0; + while (Peak(offset, out var c) && IsIndent(c)) + offset++; + + return offset; + } + + public bool Next() + { + if (_Position > _LastIndex) + return false; + + _Position++; + if (_Position > _LastIndex) + { + _Current = EmptyChar; + return false; + } + _Current = _Input[_Position]; + return true; + } + + private bool Peak(int offset, out char c) + { + c = EmptyChar; + var index = _Position + offset; + if (index > _LastIndex) + return false; + + c = _Input[index]; + return true; + } + + public void SkipIndent(int indent) + { + if (indent == 0 || EOF || IsLineEnding(_Current)) + return; + + var count = 0; + while (count < indent && IsIndent(_Current) && Next()) + count++; + } + + private static bool IsIndent(char c) + { + return char.IsWhiteSpace(c); + } + + private static bool IsLineEnding(char c) + { + return c == LF || c == CR; + } + } +} diff --git a/packages/psdocs/src/PSDocs/en-AU/PSDocs.Resources.psd1 b/packages/psdocs/src/PSDocs/en-AU/PSDocs.Resources.psd1 new file mode 100644 index 00000000..c5df32e0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/en-AU/PSDocs.Resources.psd1 @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +ConvertFrom-StringData @' +###PSLOC +DocumentNotFound=Failed to find document: {0} +PathNotFound=Path not found +SourceNotFound=Path not found +DocumentProcessFailure=Failed to process document +SectionProcessFailure=Failed to process section: {0} +###PSLOC +'@ \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/en-GB/PSDocs.Resources.psd1 b/packages/psdocs/src/PSDocs/en-GB/PSDocs.Resources.psd1 new file mode 100644 index 00000000..c5df32e0 --- /dev/null +++ b/packages/psdocs/src/PSDocs/en-GB/PSDocs.Resources.psd1 @@ -0,0 +1,12 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +ConvertFrom-StringData @' +###PSLOC +DocumentNotFound=Failed to find document: {0} +PathNotFound=Path not found +SourceNotFound=Path not found +DocumentProcessFailure=Failed to process document +SectionProcessFailure=Failed to process section: {0} +###PSLOC +'@ \ No newline at end of file diff --git a/packages/psdocs/src/PSDocs/en-US/PSDocs.Resources.psd1 b/packages/psdocs/src/PSDocs/en-US/PSDocs.Resources.psd1 new file mode 100644 index 00000000..19dbd522 --- /dev/null +++ b/packages/psdocs/src/PSDocs/en-US/PSDocs.Resources.psd1 @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +ConvertFrom-StringData @' +###PSLOC +DocumentNotFound=Failed to find document: {0} +PathNotFound=Path not found +SourceNotFound=Could not find any .Doc.ps1 script files in the path. +DocumentProcessFailure=Failed to process document +SectionProcessFailure=Failed to process section: {0} +KeywordOutsideEngine=This keyword can only be called within PSDocs. Add document definitions to .Doc.ps1 files, then execute them with Invoke-PSDocument. +###PSLOC +'@ diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Cmdlets.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Cmdlets.Doc.ps1 new file mode 100644 index 00000000..d7ea18f0 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Cmdlets.Doc.ps1 @@ -0,0 +1,42 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Document definitions for unit testing +# + +# Synopsis: Document for unit testing +Document 'WithoutInstanceName' { + $InstanceName; + $TargetObject.Object.Name; + $TargetObject.Hashtable.Name; +} + +# Synopsis: Document for unit testing +Document 'WithInstanceName' { + $InstanceName; +} + +# Synopsis: Document for unit testing +document 'WithMultiInstanceName' { + $InstanceName; +} + +# Synopsis: Document for unit testing +document 'WithEncoding' { + $InstanceName; +} + +# Synopsis: Document for unit testing +document 'WithPassThru' { + $InstanceName; +} + +# Synopsis: Document for unit testing +document 'WithMetadata' { + Metadata @{ + key1 = 'value1' + } + Section 'Test' -Force { + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Conventions.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Conventions.Doc.ps1 new file mode 100644 index 00000000..0a8139c9 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Conventions.Doc.ps1 @@ -0,0 +1,39 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Conventions for unit testing +# + +# Synopsis: An example naming convention. +Export-PSDocumentConvention 'TestNamingConvention1' { + Write-Verbose -Message 'Convention process' + $PSDocs.Document.InstanceName = [String]::Concat($PSDocs.Document.Data.testName, '_', $PSDocs.Document.Data.count); + $newPath = Join-Path -Path $PSDocs.Document.OutputPath -ChildPath 'new'; + $PSDocs.Document.OutputPath = $newPath; +} -Begin { + Write-Verbose -Message 'Convention begin'; + $PSDocs.Document.Data['count'] = 1; +} -End { + Write-Verbose -Message "Convention end"; + foreach ($output in $PSDocs.Output) { + $tocPath = Join-Path -Path $output.OutputPath -ChildPath 'toc.yaml'; + Set-Content -Path $tocPath -Value ''; + } +} + +# Synopsis: An example naming convention. +Export-PSDocumentConvention 'TestNamingConvention2' { + $PSDocs.Document.Data.count += 1; + Write-Verbose -Message 'Convention process' + $PSDocs.Document.InstanceName = [String]::Concat($PSDocs.Document.Data.testName, '_', $PSDocs.Document.Data.count); + $newPath = Join-Path -Path $PSDocs.Document.OutputPath -ChildPath 'new'; + $PSDocs.Document.OutputPath = $newPath; +} + +# Synopsis: A test document for testing conventions. +Document 'ConventionDoc1' { + $PSDocs.Document.Data.testName = $PSDocs.TargetObject.Name; + $PSDocs.Document.Data.count += 1; + 'Convention test' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Doc.ps1 new file mode 100644 index 00000000..739ebf9d --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Doc.ps1 @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Export-PSDocumentConvention 'FromFileTest1' { + +} + +Document 'FromFileTest1' { + Title 'Test title' + Metadata @{ + test = 'Test1' + } + Section 'Test' { + 'Test 1' + } +} + +Document 'FromFileTest2' { + Metadata @{ + test = 'Test2' + } + Section 'Test' { + 'Test 2' + } +} + +Document 'FromFileTest3' -Tag 'Test3' { + Metadata @{ + test = 'Test3' + } + Section 'Test' { + 'Test 3' + } +} + +Document 'FromFileTest4' -Tag 'Test4' { + Section 'Test' { + 'Test 4' + } +} + +Document 'FromFileTest5' -Tag 'Test4','Test5' { + Section 'Test' { + 'Test 5' + } +} + +Document 'ConstrainedTest1' { + Section 'Test' { + 'Test 1' + } +} + +Document 'ConstrainedTest2' { + + [Console]::WriteLine('Should fail'); +} + +Document 'WithIf' -If { $TargetObject.Generator -eq 'PSDocs' } { + Metadata @{ + Name = $PSDocs.TargetObject.Name + } + + 'EOF' +} + +Document 'WithSelector' -With 'GeneratorSelector' { + Metadata @{ + Name = $PSDocs.TargetObject.Name + } + + 'EOF' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Keyword.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Keyword.Doc.ps1 new file mode 100644 index 00000000..7e28f7b8 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Keyword.Doc.ps1 @@ -0,0 +1,287 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Document defintions for keyword unit tests +# + +#region BlockQuote + +Document 'BlockQuoteSingleMarkdown' { + 'Begin' + 'This is a single line' | BlockQuote + 'End' +} + +Document 'BlockQuoteMultiMarkdown' { + 'Begin' + @('This is the first line.' + 'This is the second line.') | BlockQuote + 'End' +} + +Document 'BlockQuoteTitleMarkdown' { + 'Begin' + 'This is a single block quote' | BlockQuote -Title 'Test' + 'End' +} + +Document 'BlockQuoteInfoMarkdown' { + 'Begin' + 'This is a single block quote' | BlockQuote -Info 'Tip' + 'End' +} + +#endregion BlockQuote + +#region Code + +Document 'CodeMarkdown' { + 'Begin' + Code { + # This is a comment + This is code + + # Another comment + And code + } + 'End' +} + +Document 'CodeMarkdownNamedFormat' { + 'Begin' + Code powershell { + Get-Content + } + 'End' +} + +Document 'CodeMarkdownEval' { + 'Begin' + $a = 1; $a += 1; $a | Code powershell; + 'End' +} + +Document 'CodeInclude' { + 'Begin' + Include 'psdocs.yml' -BaseDirectory $PSScriptRoot | Code 'yaml' + 'End' +} + +Document 'CodeJson' { + $a = [PSCustomObject]@{ + Name = 'Value' + } + $a | Code 'json' +} + +Document 'CodeYaml' { + $a = [PSCustomObject]@{ + Name = 'Value' + } + $a | Code 'yaml' +} + +#endregion Code + +#region Include + +Document 'IncludeRelative' { + Include tests/PSDocs.Tests/IncludeFile.md -BaseDirectory $TargetObject + Include IncludeFile2.md -BaseDirectory (Join-Path -Path $TargetObject -ChildPath tests/PSDocs.Tests/) +} + +Document 'IncludeAbsolute' { + Include (Join-Path -Path $TargetObject -ChildPath tests/PSDocs.Tests/IncludeFile.md) +} + +Document 'IncludeCulture' { + Include IncludeFile3.md -UseCulture -BaseDirectory tests/PSDocs.Tests/ +} + +# Synopsis: Test Include keyword with -ErrorAction SilentlyContinue +Document 'IncludeOptional' { + Include 'NotFile.md' -ErrorAction SilentlyContinue; +} + +# Synopsis: Test Include keyword with missing file +Document 'IncludeRequired' { + Include 'NotFile.md'; +} + +# Synopsis: Include and replace tokens +Document 'IncludeReplace' { + Include IncludeFile2.md -BaseDirectory $PSScriptRoot -Replace @{ + 'second' = 'third' + } +} + +#endregion Include + +#region Metadata + +Document 'MetadataSingleEntry' { + Metadata ([ordered]@{ + title = 'Test' + }) + + 'EOF' +} + +Document 'MetadataMultipleEntry' { + Metadata ([ordered]@{ + value1 = 'ABC' + value2 = 'EFG' + }) + + 'EOF' +} + +Document 'MetadataMultipleBlock' { + Metadata ([ordered]@{ + value1 = 'ABC' + }) + Section 'Test' { + 'A test section spliting metadata blocks.' + } + Metadata @{ + value2 = 'EFG' + } +} + +Document 'NoMetdata' { + Section 'Test' { + 'A test section.' + } +} + +Document 'NullMetdata' { + Metadata $Null + Section 'Test' { + 'A test section.' + } +} + +#endregion Metadata + +#region Note + +Document 'NoteSingleMarkdown' { + 'This is a single line' | Note +} + +Document 'NoteMultiMarkdown' { + @('This is the first line.' + 'This is the second line.') | Note +} + +#endregion Note + +#region Warning + +Document 'WarningSingleMarkdown' { + 'This is a single line' | Warning +} + +Document 'WarningMultiMarkdown' { + @('This is the first line.' + 'This is the second line.') | Warning +} + +#endregion Warning + +#region Section + +Document 'SectionBlockTests' { + Section 'SingleLine' { + 'This is a single line markdown section.' + } + Section 'MultiLine' { + "This is a multiline`r`ntest." + } + Section 'Empty' { + } + Section 'Forced' -Force { + } +} + +Document 'SectionIf' { + Section 'Section 1' -If { $False } { + 'Content 1' + } + Section 'Section 2' -If { $True } { + 'Content 2' + } +} + +#endregion Section + +#region Table + +Document 'TableTests' { + Get-ChildItem -Path $TargetObject -File | Where-Object -FilterScript { 'README.md','LICENSE' -contains $_.Name } | Format-Table -Property 'Name','PSIsContainer' + 'EOF' +} + +Document 'TableWithExpression' { + $object = [PSCustomObject]@{ + Name = 'Dummy' + Property = @{ + Value1 = 1 + Value2 = 2 + } + Value3 = 3 + } + $object | Table -Property Name,@{ Label = 'Value1'; Alignment = 'Left'; Width = 10; Expression = { $_.Property.Value1 }},@{ Name = 'Value2'; Alignment = 'Center'; Expression = { $_.Property.Value2 }},@{ Label = 'Value3'; Expression = { $_.Value3 }; Alignment = 'Right'; }; + 'EOF' +} + +Document 'TableSingleEntryMarkdown' { + New-Object -TypeName PSObject -Property @{ Name = 'Single' } | Table -Property Name; +} + +Document 'TableWithNull' { + Section 'Windows features' -Force { + $TargetObject.ResourceType.WindowsFeature | Table -Property Name,Ensure; + } +} + +Document 'TableWithMultilineColumn' { + $TargetObject | Table; +} + +Document 'TableWithEmptyColumn' { + 'Table1' + $TargetObject | Table -Property Name,NotValue,Value + 'Table2' + $TargetObject | Table -Property Name,NotValue + 'EOF' +} + +#endregion Table + +#region Title + +Document 'SingleTitle' { + Title 'Test title' + + 'EOF' +} + +Document 'MultipleTitle' { + Title 'Title 1' + Title 'Title 2' + + 'EOF' +} + +# Synopsis: Tests Title with empty or null string +Document 'EmptyTitle' { + $value = '' + Title $notValue + Title $value + + 'EOF' +} + +#endregion Title diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Selector.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Selector.Doc.ps1 new file mode 100644 index 00000000..92699a2f --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Selector.Doc.ps1 @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Definitions for testing selectors +# + +Document 'Selector.WithInputObject' -With 'GeneratorSelector' { + Metadata @{ + Name = $PSDocs.TargetObject.Name + } + $PSDocs.TargetObject.Name +} diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.TestObjects.yaml b/packages/psdocs/tests/PSDocs.Tests/FromFile.TestObjects.yaml new file mode 100644 index 00000000..e69de29b diff --git a/packages/psdocs/tests/PSDocs.Tests/FromFile.Variables.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/FromFile.Variables.Doc.ps1 new file mode 100644 index 00000000..f199a3ba --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/FromFile.Variables.Doc.ps1 @@ -0,0 +1,38 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Synopsis: Document for unit testing +Document 'PSAutomaticVariables' { + "PWD=$PWD;" + "PSScriptRoot=$PSScriptRoot;" + "PSCommandPath=$PSCommandPath;" +} + +# Synopsis: Test $PSDocs variable +Document 'PSDocsVariable' { + Metadata @{ + author = $PSDocs.Configuration.author.name + } + $PSDocs.Format("TargetObject.Name={0};", $PSDocs.TargetObject.Name); + if ($PSDocs.Configuration.GetBoolOrDefault('NotConfig', $True)) { + "Document.Metadata=$($Document.Metadata['author']);" + } + if ($PSDocs.Configuration.GetBoolOrDefault('Enabled', $True)) { + "Document.Enabled=true" + } +} + +# Synopsis: Test $Document variable +Document 'PSDocsDocumentVariable' { + Title '001' + Metadata @{ + author = '002' + } + "Document.Title=$($Document.Title);" + "Document.Metadata=$($Document.Metadata['author']);" +} + +# Synopsis: Test $LocalizedData variable +Document 'PSDocsLocalizedDataVariable' { + "LocalizedData.Key1=$($LocalizedData.Key1);" +} diff --git a/packages/psdocs/tests/PSDocs.Tests/IncludeFile.md b/packages/psdocs/tests/PSDocs.Tests/IncludeFile.md new file mode 100644 index 00000000..5db75b3f --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/IncludeFile.md @@ -0,0 +1 @@ +This is included from an external file. \ No newline at end of file diff --git a/packages/psdocs/tests/PSDocs.Tests/IncludeFile2.md b/packages/psdocs/tests/PSDocs.Tests/IncludeFile2.md new file mode 100644 index 00000000..bbb1dd54 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/IncludeFile2.md @@ -0,0 +1 @@ +This is a second file to include. \ No newline at end of file diff --git a/packages/psdocs/tests/PSDocs.Tests/KeywordTests.cs b/packages/psdocs/tests/PSDocs.Tests/KeywordTests.cs new file mode 100644 index 00000000..1b7171e4 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/KeywordTests.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Models; +using PSDocs.Pipeline; + +namespace PSDocs +{ + public sealed class KeywordTests + { + [Fact] + public void TableTests() + { + var actual = BuildDocument("TableWithExpression"); + Assert.Single(actual); + Assert.IsType(actual[0].Node[0]); + var table = actual[0].Node[0] as Table; + Assert.Equal("Dummy", table.Rows[0][0]); + Assert.Equal("3", table.Rows[0][3]); + + actual = BuildDocument("TableSingleEntryMarkdown"); + Assert.Single(actual); + Assert.IsType
(actual[0].Node[0]); + + actual = BuildDocument("TableWithMultilineColumn"); + Assert.Single(actual); + Assert.IsType
(actual[0].Node[0]); + Assert.True((actual[0].Node[0] as Table).Rows[0].Length > 1); + + actual = BuildDocument("TableWithEmptyColumn"); + Assert.Single(actual); + Assert.IsType
(actual[0].Node[1]); + } + + private static Document[] BuildDocument(string documentName) + { + var builder = PipelineBuilder.Invoke(GetSource(), GetOption(new string[] { documentName }), null, null); + var pipeline = builder.Build() as InvokePipeline; + var targetObject = new TargetObject(PSObject.AsPSObject(new TestModel())); + return pipeline.BuildDocument(targetObject); + } + + private static PSDocumentOption GetOption(string[] name = null) + { + var option = new PSDocumentOption(); + if (name != null && name.Length > 0) + { + option.Document.Include = name; + } + option.Output.Culture = new string[] { "en-US" }; + return option; + } + + private static Source[] GetSource() + { + var builder = new SourcePipelineBuilder(new HostContext(null, null)); + builder.Directory(GetSourcePath("FromFile.Keyword.Doc.ps1")); + return builder.Build(); + } + + private static string GetSourcePath(string fileName) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/LanguageVisitorTests.cs b/packages/psdocs/tests/PSDocs.Tests/LanguageVisitorTests.cs new file mode 100644 index 00000000..cf5eb58d --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/LanguageVisitorTests.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Pipeline; +using PSDocs.Runtime; + +namespace PSDocs +{ + public sealed class LanguageVisitorTests + { + [Fact] + public void NestedDefintion() + { + var content = @" +# Header comment +Document 'Doc1' { + +} +"; + var scriptAst = ScriptBlock.Create(content).Ast; + var visitor = new LanguageAst(GetContext()); + scriptAst.Visit(visitor); + + Assert.Null(visitor.Errors); + + content = @" +# Header comment +Document 'Doc1' { + Document 'Doc2' { + + } +} +"; + scriptAst = ScriptBlock.Create(content).Ast; + visitor = new LanguageAst(GetContext()); + scriptAst.Visit(visitor); + + Assert.Single(visitor.Errors); + } + + [Fact] + public void UnvalidDefinition() + { + var content = @" +Document '' { + +} + +Document { + +} + +Document 'Doc1'; + +Document '' { + +} + +Document 'Doc2' { + +} + +Document -Name 'Doc3' { + +} + +Document -Name 'Doc3' -Body { + +} + +"; + + var scriptAst = ScriptBlock.Create(content).Ast; + var visitor = new LanguageAst(GetContext()); + scriptAst.Visit(visitor); + + Assert.NotNull(visitor.Errors); + Assert.Equal(4, visitor.Errors.Count); + } + + private static PipelineContext GetContext() + { + return new PipelineContext(GetOption(), null, null, null, null, null); + } + + private static OptionContext GetOption(string[] name = null) + { + var option = new PSDocumentOption(); + return new OptionContext(option); + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/MarkdownProcessorTests.cs b/packages/psdocs/tests/PSDocs.Tests/MarkdownProcessorTests.cs new file mode 100644 index 00000000..912634b5 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/MarkdownProcessorTests.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Configuration; +using PSDocs.Models; +using PSDocs.Processor.Markdown; +using PSDocs.Runtime; + +namespace PSDocs +{ + public sealed class MarkdownProcessorTests + { + [Fact] + public void LineEndings() + { + var document = GetDocument(); + var actual = GetProcessor().Process(GetOption(), document).ToString(); + var expected = @"# Test document + +## Section 1 +"; + Assert.Equal(expected, actual); + } + + private static Document GetDocument() + { + var result = new Document(new DocumentContext(null)) + { + Title = "Test document" + }; + var section = new Section + { + Title = "Section 1", + Level = 2 + }; + result.Node.Add(section); + return result; + } + + private static MarkdownProcessor GetProcessor() + { + return new MarkdownProcessor(); + } + + private static PSDocumentOption GetOption() + { + return new PSDocumentOption(); + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/Models/TestModel.cs b/packages/psdocs/tests/PSDocs.Tests/Models/TestModel.cs new file mode 100644 index 00000000..c9008d48 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/Models/TestModel.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSDocs.Models +{ + internal sealed class TestModel + { + public TestModel() + { + Name = "Test"; + Description = "This is a\r\ndescription\r\nsplit\r\nover\r\nmultiple\r\nlines."; + Generator = "PSDocs"; + } + + public string Name { get; set; } + + public string Description { get; set; } + + public string Generator { get; set; } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.BlockQuote.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.BlockQuote.Tests.ps1 new file mode 100644 index 00000000..818d6f62 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.BlockQuote.Tests.ps1 @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the BlockQuote keyword +# + +[CmdletBinding()] +param () + +BeforeAll{ + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + $nl = [System.Environment]::NewLine; +} +Describe 'PSDocs -- BlockQuote keyword' -Tag BlockQuote { + + Context 'Markdown' { + BeforeAll{ + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Should handle single line input' { + $result = Invoke-PSDocument @invokeParams -Name 'BlockQuoteSingleMarkdown'; + $result | Should -Match "Begin$($nl)$($nl)\> This is a single line$($nl)$($nl)End"; + } + It 'Should handle multiline input' { + $result = Invoke-PSDocument @invokeParams -Name 'BlockQuoteMultiMarkdown'; + $result | Should -Match "Begin$($nl)$($nl)\> This is the first line.$($nl)\> This is the second line.$($nl)$($nl)End"; + } + It 'Should add title' { + $result = Invoke-PSDocument @invokeParams -Name 'BlockQuoteTitleMarkdown'; + $result | Should -Match "Begin$($nl)$($nl)\> Test$($nl)\> This is a single block quote$($nl)$($nl)End"; + } + It 'Should add info' { + $result = Invoke-PSDocument @invokeParams -Name 'BlockQuoteInfoMarkdown'; + $result | Should -Match "Begin$($nl)$($nl)\> \[!TIP\]$($nl)\> This is a single block quote$($nl)$($nl)End"; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 new file mode 100644 index 00000000..a3c8643f --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Code.Tests.ps1 @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Code keyword +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + $nl = [System.Environment]::NewLine; +} + +Describe 'PSDocs -- Code keyword' -Tag Code { + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Should have generated output' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeMarkdown'; + $result | Should -Match "Begin$($nl)$($nl)``````powershell$($nl)`# This is a comment$($nl)This is code$($nl)$($nl)`# Another comment$($nl)And code$($nl)``````$($nl)$($nl)End"; + } + It 'Code markdown with named format' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeMarkdownNamedFormat'; + $result | Should -Match "Begin$($nl)$($nl)``````powershell$($nl)Get-Content$($nl)``````$($nl)$($nl)End"; + } + It 'Code markdown with evaluation' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeMarkdownEval'; + $result | Should -Match "Begin$($nl)$($nl)``````powershell$($nl)2$($nl)``````$($nl)$($nl)End"; + } + It 'Code markdown with include' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeInclude'; + $result | Should -Match "Begin$($nl)$($nl)``````yaml$($nl)generator: PSDocs$($nl)``````$($nl)$($nl)End"; + } + It 'Code markdown with JSON conversion' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeJson'; + $result | Should -Match "``````json$($nl){$($nl) `"Name`": `"Value`"$($nl)}$($nl)``````"; + } + It 'Code markdown with YAML conversion' { + $result = Invoke-PSDocument @invokeParams -Name 'CodeYaml'; + $result | Should -Match "``````yaml$($nl)name: Value$($nl)``````"; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 new file mode 100644 index 00000000..5d1e5b7d --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Common.Tests.ps1 @@ -0,0 +1,364 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for core PSDocs functionality +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath.Path -ChildPath out/modules/PSDocs) -Force; + $outputPath = Join-Path -Path $rootPath.Path -ChildPath out/tests/PSDocs.Tests/Common; + Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction Ignore; + $Null = New-Item -Path $outputPath -ItemType Directory -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + + $dummyObject = New-Object -TypeName PSObject -Property @{ + Object = [PSObject]@{ + Name = 'ObjectName' + Value = 'ObjectValue' + } + + Hashtable = @{ + Name = 'HashName' + Value = 'HashValue' + } + } + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Cmdlets.Doc.ps1'; +} + +Describe 'PSDocs instance names' -Tag 'Common', 'InstanceName' { + Context 'Generate a document without an instance name' { + BeforeAll { + $invokeParams = @{ + Path = $docFilePath + InputObject = $dummyObject + OutputPath = $outputPath + } + $result = Invoke-PSDocument @invokeParams -Name 'WithoutInstanceName'; + } + It 'Should generate an output named WithoutInstanceName.md' { + Test-Path -Path $result.FullName | Should -Be $True; + $outputDoc = Get-Content -Path $result.FullName -Raw; + $outputDoc | Should -Match 'WithoutInstanceName'; + $outputDoc | Should -Match 'ObjectName'; + $outputDoc | Should -Match 'HashName'; + } + } + + Context 'Generate a document with an instance name' { + BeforeAll { + $invokeParams = @{ + Path = $docFilePath + InputObject = $dummyObject + OutputPath = $outputPath + } + $null = Invoke-PSDocument @invokeParams -InstanceName 'Instance1' -Name 'WithInstanceName'; + } + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithInstanceName.md" | Should -Be $False; + Test-Path -Path "$outputPath\Instance1.md" | Should -Be $True; + Get-Content -Path "$outputPath\Instance1.md" -Raw | Should -Match 'Instance1'; + } + } + + Context 'Generate a document with multiple instance names' { + BeforeAll { + $invokeParams = @{ + Path = $docFilePath + InputObject = $dummyObject + OutputPath = $outputPath + } + $null = Invoke-PSDocument @invokeParams -InstanceName 'Instance2', 'Instance3' -Name 'WithMultiInstanceName'; + } + It 'Should not create a output with the document name' { + Test-Path -Path "$outputPath\WithMultiInstanceName.md" | Should -Be $False; + } + It 'Should generate an output named Instance2.md' { + Test-Path -Path "$outputPath\Instance2.md" | Should -Be $True; + Get-Content -Path "$outputPath\Instance2.md" -Raw | Should -Match 'Instance2'; + } + It 'Should generate an output named Instance3.md' { + Test-Path -Path "$outputPath\Instance3.md" | Should -Be $True; + Get-Content -Path "$outputPath\Instance3.md" -Raw | Should -Match 'Instance3'; + } + } + + Context 'Generate a document with a specific encoding' { + BeforeAll { + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + OutputPath = $outputPath + } + $encodings = @('UTF8', 'UTF7', 'Unicode', 'ASCII', 'UTF32') + } + # Check each encoding can be written then read + foreach ($encoding in $encodings) { + $currentEncoding = $encoding + It "Should generate $encoding encoded content" { + Invoke-PSDocument @invokeParams -InstanceName "With$currentEncoding" -Encoding $currentEncoding -Name 'WithEncoding'; + Get-Content -Path (Join-Path -Path $outputPath -ChildPath "With$currentEncoding.md") -Encoding $currentEncoding | Out-String | Should -Match "^(With$currentEncoding(\r|\n|\r\n))$"; + } + } + } + + Context 'With -PassThru' { + BeforeAll { + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Should return results' { + $result = Invoke-PSDocument @invokeParams -Name 'WithPassThru'; + $result | Should -Match 'WithPassThru'; + } + } +} + +Describe 'Invoke-PSDocument' -Tag 'Cmdlet', 'Common', 'Invoke-PSDocument', 'FromPath' { + + Context 'With -Path' { + BeforeAll { + $testObject = [PSCustomObject]@{}; + } + It 'Should match name' { + # Only generate documents for the named document + $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name FromFileTest2; + Test-Path -Path "$outputPath\FromFileTest1.md" | Should -Be $False; + Test-Path -Path "$outputPath\FromFileTest2.md" | Should -Be $True; + Test-Path -Path "$outputPath\FromFileTest3.md" | Should -Be $False; + } + It 'Should match single tag' { + # Only generate for documents with matching tag + $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Tag Test3; + Test-Path -Path "$outputPath\FromFileTest1.md" | Should -Be $False; + Test-Path -Path "$outputPath\FromFileTest3.md" | Should -Be $True; + } + It 'Should match all tags' { + # Only generate for documents with all matching tags + $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Tag Test4, Test5; + Test-Path -Path "$outputPath\FromFileTest1.md" | Should -Be $False; + Test-Path -Path "$outputPath\FromFileTest4.md" | Should -Be $False; + Test-Path -Path "$outputPath\FromFileTest5.md" | Should -Be $True; + } + It 'Should generate exception' { + { $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name InvalidCommand -ErrorAction Stop } | Should -Throw -ExceptionType PSDocs.Pipeline.RuntimeException; + $Error[0].Exception.Message | Should -Match '^(The term ''New-PSDocsInvalidCommand'' is not recognized)'; + { $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name InvalidCommandWithSection -ErrorAction Stop } | Should -Throw -ExceptionType PSDocs.Pipeline.RuntimeException; + $Error[0].Exception.Message | Should -Match '^(The term ''New-PSDocsInvalidCommand'' is not recognized)'; + { $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name WithWriteError -ErrorAction Stop } | Should -Throw; + $Error[0].Exception.Message | Should -Match 'Verify Write-Error is raised as an exception'; + } + } + + Context 'With -Module' { + BeforeAll { + $testObject = [PSCustomObject]@{}; + $testModuleSourcePath = Join-Path $here -ChildPath 'TestModule'; + $Null = Import-Module $testModuleSourcePath -Force; + $result = @($testObject | Invoke-PSDocument -Module 'TestModule' -Name 'TestDocument1' -Culture 'en-US', 'en-AU', 'en-ZZ'); + } + + It 'Returns documents' { + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 3; + $result[0].Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries) | Should -Be "Culture=en-US"; + $result[1].Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries) | Should -Be "Culture=en-AU"; + $result[2].Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries) | Should -Be "Culture=en"; + } + } + + Context 'With -PassThru' { + BeforeAll { + $testObject = [PSCustomObject]@{}; + $result = @($testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name FromFileTest1, FromFileTest2 -PassThru); + } + It 'Should return results' { + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result[0] | Should -Match "`# Test title"; + $result[1] | Should -Match "test: Test2"; + } + } + + Context 'With -InputPath' { + BeforeAll { + $result = @(Invoke-PSDocument -Path $here -OutputPath $outputPath -InputPath $here/*.yml -Name FromFileTest1, FromFileTest2 -PassThru); + } + It 'Should return results' { + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 4; + $result[0] | Should -Match "`# Test title"; + $result[1] | Should -Match "test: Test2"; + } + } + + Context 'With constrained language' { + BeforeAll { + $testObject = [PSCustomObject]@{}; + } + # Check that '[Console]::WriteLine('Should fail')' is not executed + It 'Should fail to execute blocked code' { + { $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name 'ConstrainedTest2' -Option @{ 'Execution.LanguageMode' = 'ConstrainedLanguage' } -ErrorAction Stop } | Should -Throw 'Cannot invoke method. Method invocation is supported only on core types in this language mode.'; + Test-Path -Path "$outputPath\ConstrainedTest2.md" | Should -Be $False; + } + It 'Checks if DeviceGuard is enabled' { + Mock -CommandName IsDeviceGuardEnabled -ModuleName PSDocs -Verifiable -MockWith { + return $True; + } + $testObject | Invoke-PSDocument -Path $here -OutputPath $outputPath -Name 'ConstrainedTest1'; + Assert-MockCalled -CommandName IsDeviceGuardEnabled -ModuleName PSDocs -Times 1; + } + } +} + +Describe 'Get-PSDocument' -Tag 'Cmdlet', 'Common', 'Get-PSDocument' { + Context 'With -Module' { + BeforeAll { + $testModuleSourcePath = Join-Path $here -ChildPath 'TestModule' + $mock = Mock -ModuleName 'PSDocs' LoadModule + Import-Module $testModuleSourcePath -Force + if ($Null -ne (Get-Module -Name TestModule -ErrorAction SilentlyContinue)) { + $Null = Remove-Module -Name TestModule; + } + $Null = Import-Module $testModuleSourcePath -Force; + $result = @(Get-PSDocument -Module 'TestModule'); + $currentLoadingPreference = Get-Variable -Name PSModuleAutoLoadingPreference -ErrorAction SilentlyContinue -ValueOnly + $Env:PSModulePath = $here; + } + It 'Returns documents' { + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result.Id | Should -BeIn 'TestModule\TestDocument1', 'TestModule\TestDocument2'; + } + + + It 'Loads module with preference' { + try { + # Test negative case + $Global:PSModuleAutoLoadingPreference = [System.Management.Automation.PSModuleAutoLoadingPreference]::None + Write-Host "Calling Get-PSDocument with preference set to None" + $Null = Get-PSDocument -Module 'TestModule' + #Assert-MockCalled -CommandName 'LoadModule' -ModuleName 'PSDocs' -Times 0 -Scope 'It' + $mock | Should -Invoke LoadModule -ModuleName 'PSDocs' -Times 0 -Scope 'It' + # Test positive case + $Global:PSModuleAutoLoadingPreference = [System.Management.Automation.PSModuleAutoLoadingPreference]::All + Write-Host "Calling Get-PSDocument with preference set to All" + $Null = Get-PSDocument -Module 'TestModule' + #Assert-MockCalled -CommandName 'LoadModule' -ModuleName 'PSDocs' -Times 1 -Scope 'It' -Exactly + $mock | Should -Invoke LoadModule -ModuleName 'PSDocs' -Times 1 -Scope 'It' + + Assert-VerifiableMocks + } + finally { + if ($Null -eq $currentLoadingPreference) { + Remove-Variable -Name PSModuleAutoLoadingPreference -Force -ErrorAction SilentlyContinue + } + else { + $Global:PSModuleAutoLoadingPreference = $currentLoadingPreference + } + } + } + + + It 'Use modules already loaded' { + Mock -CommandName 'GetAutoloadPreference' -ModuleName 'PSDocs' -MockWith { + return [System.Management.Automation.PSModuleAutoLoadingPreference]::All; + } + Mock -CommandName 'LoadModule' -ModuleName 'PSDocs'; + $Null = Import-Module $testModuleSourcePath -Force; + $result = @(Get-PSDocument -Module 'TestModule') + Assert-MockCalled -CommandName 'LoadModule' -ModuleName 'PSDocs' -Times 0 -Scope 'It'; + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result.Id | Should -BeIn 'TestModule\TestDocument1', 'TestModule\TestDocument2'; + } + + if ($Null -ne (Get-Module -Name TestModule -ErrorAction SilentlyContinue)) { + $Null = Remove-Module -Name TestModule; + } + + It 'Handles path spaces' { + # Copy file + $testParentPath = Join-Path -Path $outputPath -ChildPath 'Program Files\'; + $testDestinationPath = Join-Path -Path $testParentPath -ChildPath 'FromFile.Doc.ps1'; + if (!(Test-Path -Path $testParentPath)) { + $Null = New-Item -Path $testParentPath -ItemType Directory -Force; + } + $Null = Copy-Item -Path $docFilePath -Destination $testDestinationPath -Force; + + $result = @(Get-PSDocument -Path $testDestinationPath); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -BeGreaterOrEqual 6; + + # Copy module to test path + $testModuleDestinationPath = Join-Path -Path $testParentPath -ChildPath 'TestModule'; + $Null = Copy-Item -Path $testModuleSourcePath -Destination $testModuleDestinationPath -Recurse -Force; + + # Test modules with spaces in paths + $Null = Import-Module $testModuleDestinationPath -Force; + $result = @(Get-PSDocument -Module 'TestModule'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 2; + $result[0].Id | Should -Be 'TestModule\TestDocument1'; + } + + if ($Null -ne (Get-Module -Name TestModule -ErrorAction SilentlyContinue)) { + $Null = Remove-Module -Name TestModule; + } + + It 'Returns module and path documents' { + $Null = Import-Module $testModuleSourcePath -Force; + $result = @(Get-PSDocument -Path $testModuleSourcePath -Module 'TestModule'); + $result | Should -Not -BeNullOrEmpty; + $result.Length | Should -Be 4; + $result.Id | Should -BeIn 'TestModule\TestDocument1', 'TestModule\TestDocument2', '.\TestDocument1', '.\TestDocument2'; + } + + if ($Null -ne (Get-Module -Name TestModule -ErrorAction SilentlyContinue)) { + $Null = Remove-Module -Name TestModule; + } + + if ($Null -ne (Get-Module -Name TestModule -ErrorAction SilentlyContinue)) { + $Null = Remove-Module -Name TestModule; + } + } +} + +Describe 'Get-PSDocumentHeader' -Tag 'Cmdlet', 'Common', 'Get-PSDocumentHeader' { + Context 'With -Path' { + BeforeAll { + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + OutputPath = $outputPath + } + } + + It 'Get Metadata header' { + $result = Invoke-PSDocument @invokeParams -Name 'WithMetadata'; + $result = Get-PSDocumentHeader -Path $outputPath; + $result | Should -Not -BeNullOrEmpty; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Conventions.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Conventions.Tests.ps1 new file mode 100644 index 00000000..5dfa0701 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Conventions.Tests.ps1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the PSDocs conventions +# + +[CmdletBinding()] +param () +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $outputPath = Join-Path -Path $rootPath -ChildPath out/tests/PSDocs.Tests/Conventions; + $here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Conventions' -Tag Conventions { + + + Context '-Convention' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Conventions.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + OutputPath = $outputPath + } + } + It 'Generate output' { + # Singe convention + $result = Invoke-PSDocument @invokeParams -Name 'ConventionDoc1' -Convention 'TestNamingConvention1'; + $result | Should -BeLike '*TestObject_2.md'; + $testFile = Join-Path -Path $outputPath -ChildPath 'new/TestObject_2.md'; + Test-Path -Path $testFile | Should -Be $True; + $tocFile = Join-Path -Path $outputPath -ChildPath 'new/toc.yaml'; + Test-Path -Path $tocFile | Should -Be $True; + + # Multiple conventions + $result = Invoke-PSDocument @invokeParams -Name 'ConventionDoc1' -Convention 'TestNamingConvention1', 'TestNamingConvention2'; + $result | Should -BeLike '*TestObject_3.md'; + $testFile = Join-Path -Path $outputPath -ChildPath 'new/new/TestObject_3.md'; + Test-Path -Path $testFile | Should -Be $True; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Include.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Include.Tests.ps1 new file mode 100644 index 00000000..168befe5 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Include.Tests.ps1 @@ -0,0 +1,77 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Include keyword +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + + $outputPath = Join-Path -Path $rootPath -ChildPath out/tests/PSDocs.Tests/Include; + Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction Ignore; + $Null = New-Item -Path $outputPath -ItemType Directory -Force; + $here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Include keyword' -Tag Include { + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{}; + $invokeParams = @{ + Path = $docFilePath + OutputPath = $outputPath + ErrorAction = [System.Management.Automation.ActionPreference]::Stop + } + } + + It 'Should include a relative path' { + $outputDoc = "$outputPath\IncludeRelative.md"; + $Null = Invoke-PSDocument @invokeParams -InputObject $rootPath -Name 'IncludeRelative'; + + Test-Path -Path $outputDoc | Should -Be $True; + $outputDoc | Should -FileContentMatch 'This is included from an external file.'; + $outputDoc | Should -FileContentMatch 'This is a second file to include.'; + } + + It 'Should include an absolute path' { + $outputDoc = "$outputPath\IncludeAbsolute.md"; + $Null = Invoke-PSDocument @invokeParams -InputObject $rootPath -Name 'IncludeAbsolute'; + + Test-Path -Path $outputDoc | Should -Be $True; + $outputDoc | Should -FileContentMatch 'This is included from an external file.'; + } + + It 'Should include from culture' { + $Null = $testObject | Invoke-PSDocument @invokeParams -Culture 'en-AU', 'en-US' -Name 'IncludeCulture'; + + $outputDoc = "$outputPath\en-AU\IncludeCulture.md"; + Test-Path -Path $outputDoc | Should -Be $True; + $outputDoc | Should -FileContentMatch 'This is en-AU.'; + + $outputDoc = "$outputPath\en-US\IncludeCulture.md"; + Test-Path -Path $outputDoc | Should -Be $True; + $outputDoc | Should -FileContentMatch 'This is en-US.'; + } + + It 'Should include when file exists' { + $Null = $testObject | Invoke-PSDocument @invokeParams -Name 'IncludeOptional'; + { $Null = $testObject | Invoke-PSDocument @invokeParams -Name 'IncludeRequired'; } | Should -Throw -Because 'PSDocs.Runtime.IncludeNotFound'; + } + + It 'Should replace tokens' { + $result = $testObject | Invoke-PSDocument @invokeParams -Name 'IncludeReplace' -PassThru; + $result | Should -BeLike 'This is a third file to include.*' + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Metadata.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Metadata.Tests.ps1 new file mode 100644 index 00000000..cb382612 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Metadata.Tests.ps1 @@ -0,0 +1,60 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Metadata keyword +# + +[CmdletBinding()] +param ( + +) +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + $dummyObject = New-Object -TypeName PSObject; +} +Describe 'PSDocs -- Metadata keyword' -Tag Metadata { + + Context 'Markdown' { + BeforeAll{ + + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $invokeParams = @{ + Path = $docFilePath + InputObject = $dummyObject + PassThru = $True + } + } + It 'Metadata single entry' { + $result = Invoke-PSDocument @invokeParams -Name 'MetadataSingleEntry'; + $result | Should -Match '---(\r|\n|\r\n)title: Test(\r|\n|\r\n)---'; + } + + It 'Metadata multiple entries' { + $result = Invoke-PSDocument @invokeParams -Name 'MetadataMultipleEntry'; + $result | Should -Match '---(\r|\n|\r\n)value1: ABC(\r|\n|\r\n)value2: EFG(\r|\n|\r\n)---'; + } + + It 'Metadata multiple blocks' { + $result = Invoke-PSDocument @invokeParams -Name 'MetadataMultipleBlock'; + $result | Should -Match '---(\r|\n|\r\n)value1: ABC(\r|\n|\r\n)value2: EFG(\r|\n|\r\n)---'; + } + + It 'Document without Metadata block' { + $result = Invoke-PSDocument @invokeParams -Name 'NoMetdata'; + $result | Should -Not -Match '---(\r|\n|\r\n)'; + } + + It 'Document null Metadata block' { + $result = Invoke-PSDocument @invokeParams -Name 'NullMetdata'; + $result | Should -Not -Match '---(\r|\n|\r\n)'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 new file mode 100644 index 00000000..03bef7c7 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Note.Tests.ps1 @@ -0,0 +1,46 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Note keyword +# + +[CmdletBinding()] +param ( + +) +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Note keyword' -Tag Note { + + + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Should handle single line input' { + $result = Invoke-PSDocument @invokeParams -Name 'NoteSingleMarkdown'; + $result | Should -Match '\> \[\!NOTE\](\r|\n|\r\n)> This is a single line'; + } + It 'Should handle multiline input' { + $result = Invoke-PSDocument @invokeParams -Name 'NoteMultiMarkdown'; + $result | Should -Match '\> \[\!NOTE\](\r|\n|\r\n)> This is the first line\.(\r|\n|\r\n)> This is the second line\.'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Options.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Options.Tests.ps1 new file mode 100644 index 00000000..f54acc39 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Options.Tests.ps1 @@ -0,0 +1,298 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the options handling +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + $here = Split-Path -Parent $MyInvocation.MyCommand.Definition + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $emptyOptionsFilePath = Join-Path -Path $here -ChildPath 'psdocs.yml'; +} +Describe 'New-PSDocumentOption' -Tag 'Option' { + Context 'Read psdocs.yml' { + BeforeAll { + try { + Push-Location -Path $here; + It 'can read default YAML' { + $option = New-PSDocumentOption; + $option.Generator | Should -Be 'PSDocs'; + } + } + finally { + Pop-Location; + } + } + } + + Context 'Read Configuration' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Configuration.Count | Should -Be 0; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Configuration.Key1' = 'Value1'; 'Configuration.BoolValue' = $True }; + $option.Configuration.Key1 | Should -Be 'Value1'; + $option.Configuration.BoolValue | Should -Be $True; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Configuration.Key1 | Should -Be 'Value2'; + $option.Configuration.'UnitTests.String.1' | Should -Be 'Config string 1'; + $option.Configuration.'UnitTests.Bool.1' | Should -Be $True; + } + } + + Context 'Read Execution.LanguageMode' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Execution.LanguageMode | Should -Be 'FullLanguage'; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Execution.LanguageMode' = 'ConstrainedLanguage' }; + $option.Execution.LanguageMode | Should -Be 'ConstrainedLanguage'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Execution.LanguageMode | Should -Be 'ConstrainedLanguage' + } + } + + Context 'Read Input.Format' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Input.Format | Should -Be 'Detect'; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Input.Format' = 'Yaml' }; + $option.Input.Format | Should -Be 'Yaml'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Input.Format | Should -Be 'Yaml' + } + + It 'from Environment' { + try { + $Env:PSDOCS_INPUT_FORMAT = 'Yaml'; + $option = New-PSDocumentOption; + $option.Input.Format | Should -Be 'Yaml'; + } + finally { + Remove-Item 'Env:PSDOCS_INPUT_FORMAT' -Force; + } + } + + It 'from parameter' { + $option = New-PSDocumentOption -Format 'Yaml' -Path $emptyOptionsFilePath; + $option.Input.Format | Should -Be 'Yaml'; + } + } + + Context 'Read Input.ObjectPath' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Input.ObjectPath | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Input.ObjectPath' = 'items' }; + $option.Input.ObjectPath | Should -Be 'items'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Input.ObjectPath | Should -Be 'items' + } + + It 'from Environment' { + try { + $Env:PSDOCS_INPUT_OBJECTPATH = 'items'; + $option = New-PSDocumentOption; + $option.Input.ObjectPath | Should -Be 'items'; + } + finally { + Remove-Item 'Env:PSDOCS_INPUT_OBJECTPATH' -Force; + } + } + + It 'from parameter' { + $option = New-PSDocumentOption -InputObjectPath 'items' -Path $emptyOptionsFilePath; + $option.Input.ObjectPath | Should -Be 'items'; + } + } + + Context 'Read Input.PathIgnore' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Input.PathIgnore | Should -Be $Null; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Input.PathIgnore' = 'ignore.cs' }; + $option.Input.PathIgnore | Should -Be 'ignore.cs'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Input.PathIgnore | Should -Be '*.Designer.cs'; + } + + It 'from Environment' { + try { + # With single item + $Env:PSDOCS_INPUT_PATHIGNORE = 'ignore.cs'; + $option = New-PSDocumentOption; + $option.Input.PathIgnore | Should -Be 'ignore.cs'; + + # With array + $Env:PSDOCS_INPUT_PATHIGNORE = 'ignore.cs;*.Designer.cs'; + $option = New-PSDocumentOption; + $option.Input.PathIgnore | Should -Be 'ignore.cs', '*.Designer.cs'; + } + finally { + Remove-Item 'Env:PSDOCS_INPUT_PATHIGNORE' -Force; + } + } + + It 'from parameter' { + $option = New-PSDocumentOption -InputPathIgnore 'ignore.cs' -Path $emptyOptionsFilePath; + $option.Input.PathIgnore | Should -Be 'ignore.cs'; + } + } + + Context 'Read Markdown.Encoding' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Markdown.Encoding | Should -Be 'Default'; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Markdown.Encoding' = 'UTF8' }; + $option.Markdown.Encoding | Should -Be 'UTF8'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Markdown.Encoding | Should -Be 'UTF8'; + } + } + + Context 'Read Markdown.WrapSeparator' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Markdown.WrapSeparator | Should -Be ' '; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Markdown.WrapSeparator' = 'ZZZ' }; + $option.Markdown.WrapSeparator | Should -Be 'ZZZ'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Markdown.WrapSeparator | Should -Be 'ZZZ'; + } + } + + Context 'Read Markdown.SkipEmptySections' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Markdown.SkipEmptySections | Should -Be $True; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Markdown.SkipEmptySections' = $False }; + $option.Markdown.SkipEmptySections | Should -Be $False; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Markdown.SkipEmptySections | Should -Be $False; + } + } + + Context 'Read Markdown.ColumnPadding' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Markdown.ColumnPadding | Should -Be 'MatchHeader'; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Markdown.ColumnPadding' = 'Single' }; + $option.Markdown.ColumnPadding | Should -Be 'Single'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Markdown.ColumnPadding | Should -Be 'Single'; + } + } + + Context 'Read Markdown.UseEdgePipes' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Markdown.UseEdgePipes | Should -Be 'WhenRequired'; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Markdown.UseEdgePipes' = 'Always' }; + $option.Markdown.UseEdgePipes | Should -Be 'Always'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Markdown.UseEdgePipes | Should -Be 'Always'; + } + } + + Context 'Read Output.Culture' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Output.Culture | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Output.Culture' = 'en-ZZ' }; + $option.Output.Culture | Should -Be 'en-ZZ'; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Output.Culture | Should -Be 'en-ZZ'; + } + } + + Context 'Read Output.Path' { + It 'from default' { + $option = New-PSDocumentOption -Default; + $option.Output.Path | Should -BeNullOrEmpty; + } + + It 'from Hashtable' { + $option = New-PSDocumentOption -Option @{ 'Output.Path' = $here }; + $option.Output.Path | Should -Be $here; + } + + It 'from YAML' { + $option = New-PSDocumentOption -Option (Join-Path -Path $here -ChildPath 'PSDocs.Tests.yml'); + $option.Output.Path | Should -Be 'out/'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 new file mode 100644 index 00000000..7c92524b --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Section.Tests.ps1 @@ -0,0 +1,52 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Section keyword +# + +[CmdletBinding()] +param () +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + $dummyObject = New-Object -TypeName PSObject; +} +Describe 'PSDocs -- Section keyword' -Tag Section { + + + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $invokeParams = @{ + Path = $docFilePath + InputObject = $dummyObject + PassThru = $True + } + } + + It 'With defaults' { + $result = Invoke-PSDocument @invokeParams -Name 'SectionBlockTests' -InstanceName 'Section'; + $result | Should -Match "`#`# SingleLine(\r|\n|\r\n){2,}This is a single line markdown section.(\r|\n|\r\n){2,}`#`# MultiLine(\r|\n|\r\n){2,}This is a multiline(\r|\n|\r\n)test."; + $result | Should -Not -Match "`#`# Empty"; + $result | Should -Match "`#`# Forced"; + } + + It 'With -Force' { + $result = Invoke-PSDocument @invokeParams -Name 'SectionBlockTests' -InstanceName 'Section2' -Option @{ 'Markdown.SkipEmptySections' = $False }; + $result | Should -Match "`#`# Empty"; + } + + It 'With -If' { + $result = Invoke-PSDocument @invokeParams -Name 'SectionIf'; + $result | Should -Match "`#`# Section 2(\r|\n|\r\n){2,}Content 2"; + $result | Should -Not -Match "`#`# Section 1(\r|\n|\r\n){2,}Content 1"; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Selector.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Selector.Tests.ps1 new file mode 100644 index 00000000..7484331e --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Selector.Tests.ps1 @@ -0,0 +1,73 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for PSDocs selectors +# + +[CmdletBinding()] +param () +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + + $outputPath = Join-Path -Path $rootPath -ChildPath out/tests/PSDocs.Tests/Selector; + Remove-Item -Path $outputPath -Force -Recurse -Confirm:$False -ErrorAction Ignore; + $Null = New-Item -Path $outputPath -ItemType Directory -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + + $dummyObject = @( + [PSObject]@{ + Name = 'ObjectName' + Value = 'ObjectValue' + generator = 'PSDocs' + } + + [PSObject]@{ + Name = 'HashName' + Value = 'HashValue' + generator = 'notPSDocs' + } + ) + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Selector.Doc.ps1'; + $selectorFilePath = Join-Path -Path $here -ChildPath 'Selectors.Doc.yaml'; +} +Describe 'PSDocs selectors' -Tag 'Selector' { + Context 'Invoke definitions' { + BeforeAll { + + $invokeParams = @{ + Path = @($docFilePath, $selectorFilePath) + } + + It 'Generates documentation for matching objects' { + $result = @($dummyObject | Invoke-PSDocument @invokeParams -Name 'Selector.WithInputObject' -PassThru); + $result | Should -Not -BeNullOrEmpty; + $result | Should -Not -Be 'Name: HashName'; + } + } + } + + Context 'Get definitions' { + It 'With selector' { + $getParams = @{ + Path = @($docFilePath, $selectorFilePath) + } + $result = @(Get-PSDocument @getParams) + $result | Should -Not -BeNullOrEmpty; + } + + It 'Missing selector' { + $getParams = @{ + Path = @($docFilePath) + } + { Get-PSDocument @getParams -ErrorAction Stop } | Should -Throw -ErrorId 'PSDocs.Parse.SelectorNotFound'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 new file mode 100644 index 00000000..01c4f6aa --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Table.Tests.ps1 @@ -0,0 +1,85 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Table keyword +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Table keyword' -Tag Table { + + + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{}; + $invokeParams = @{ + Path = $docFilePath + PassThru = $True + } + } + It 'With defaults' { + $result = Invoke-PSDocument @invokeParams -InputObject $rootPath -InstanceName Table -Name 'TableTests' -Option @{ + 'Markdown.ColumnPadding' = 'None' + 'Markdown.UseEdgePipes' = 'Always' + }; + $result | Should -Match '(\r|\n|\r\n)|LICENSE|False|(\r|\n|\r\n)'; + $result | Should -Match '(\r|\n|\r\n)|README.md|False|(\r|\n|\r\n)'; + } + + It 'With property expressions' { + $result = $testObject | Invoke-PSDocument @invokeParams -Name 'TableWithExpression'; + $result | Should -Match '(\r|\n|\r\n)---- \| :----- \| :----: \| -----:(\r|\n|\r\n)' + $result | Should -Match '(\r|\n|\r\n)Dummy \| 1 \| 2 \| 3(\r|\n|\r\n){2,}EOF'; + } + + It 'With single entry' { + $result = $testObject | Invoke-PSDocument @invokeParams -Name 'TableSingleEntryMarkdown'; + $result | Should -Match '\| Name \|(\r|\n|\r\n)| -{1,} \|(\r|\n|\r\n)| Single \|'; + } + + It 'With null' { + $result = Invoke-PSDocument @invokeParams -Name 'TableWithNull' -InputObject @{ ResourceType = @{ WindowsFeature = @() } }; + $result | Should -Match "`#`# Windows features(\r|\n|\r\n)$"; + } + + It 'With multiline column' { + $testObject = [PSCustomObject]@{ + Name = 'Test' + Description = "This is a`r`ndescription`r`nsplit`r`nover`r`nmultiple`r`nlines." + } + + $result = Invoke-PSDocument @invokeParams -Name 'TableWithMultilineColumn' -InputObject $testObject; + $result | Should -Match 'This is a description split over multiple lines\.'; + + # With separator + $option = @{ + 'Markdown.WrapSeparator' = '
' + } + $result = Invoke-PSDocument @invokeParams -Name 'TableWithMultilineColumn' -InputObject $testObject -InstanceName 'TableWithMultilineColumnCustom' -Option $option; + $result | Should -Match 'This is a\
description\
split\
over\
multiple\
lines\.'; + } + + It 'With null column' { + $testObject = [PSCustomObject]@{ + Name = 'Test' + Value = 'Value' + } + $result = Invoke-PSDocument @invokeParams -Name 'TableWithEmptyColumn' -InputObject $testObject -InstanceName 'TableWithEmptyColumn'; + $result | Should -Match 'Name \| NotValue \| Value(\r|\n|\r\n)---- \| -------- \| -----(\r|\n|\r\n)Test \| \| Value'; + $result | Should -Match 'Name \| NotValue(\r|\n|\r\n)---- \| --------(\r|\n|\r\n)Test \|(\r|\n|\r\n)'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.csproj b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.csproj new file mode 100644 index 00000000..cc386cad --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.csproj @@ -0,0 +1,42 @@ + + + + net7.0 + {4870f00e-8c1b-4df3-95ef-eddaa2e57205} + true + false + PSDocs + Full + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + PreserveNewest + + + + diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.yml b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.yml new file mode 100644 index 00000000..50b5d6d5 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Tests.yml @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# These are test options + +configuration: + Key1: Value2 + UnitTests.String.1: Config string 1 + UnitTests.Bool.1: true + +execution: + languageMode: ConstrainedLanguage + +input: + format: Yaml + objectPath: 'items' + pathIgnore: + - '*.Designer.cs' + +markdown: + encoding: UTF8 + wrapSeparator: 'ZZZ' + skipEmptySections: false + columnPadding: Single + useEdgePipes: Always + +output: + culture: 'en-ZZ' + path: out/ diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Title.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Title.Tests.ps1 new file mode 100644 index 00000000..8021ce92 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Title.Tests.ps1 @@ -0,0 +1,53 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Title keyword +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Title keyword' -Tag Title { + + + Context 'Markdown' { + BeforeAll { + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + ErrorAction = [System.Management.Automation.ActionPreference]::Stop + } + } + It 'With single title' { + $result = Invoke-PSDocument @invokeParams -Name 'SingleTitle'; + $result | Should -Match "^(`# Test title(\r|\n|\r\n))"; + } + It 'With multiple titles' { + $result = Invoke-PSDocument @invokeParams -Name 'MultipleTitle'; + $result | Should -Match "^(`# Title 2(\r|\n|\r\n))"; + } + It 'With empty title' { + $Null = Invoke-PSDocument @invokeParams -Name 'EmptyTitle' -WarningAction SilentlyContinue -WarningVariable outWarnings; + $outWarnings = @($outWarnings); + $outWarnings | Should -Not -BeNullOrEmpty; + $outWarnings.Length | Should -Be 2; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Variables.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Variables.Tests.ps1 new file mode 100644 index 00000000..b4c88e17 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Variables.Tests.ps1 @@ -0,0 +1,71 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the PSDocs automatic variables +# + +[CmdletBinding()] +param () + +BeforeAll { + # Setup error handling + $ErrorActionPreference = 'Stop'; + Set-StrictMode -Version latest; + + # Setup tests paths + $rootPath = $PWD; + Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; + $here = (Resolve-Path $PSScriptRoot).Path; + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Variables.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } +} +Describe 'PSDocs variables' -Tag 'Variables' { + + Context 'PowerShell automatic variables' { + BeforeAll { + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Paths' { + $result = (Invoke-PSDocument @invokeParams -Name 'PSAutomaticVariables' | Out-String).Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries); + $result | Where-Object -FilterScript { $_ -like "PWD=*" } | Should -Be "PWD=$PWD;"; + $result | Where-Object -FilterScript { $_ -like "PSScriptRoot=*" } | Should -Be "PSScriptRoot=$PSScriptRoot;"; + $result | Where-Object -FilterScript { $_ -like "PSCommandPath=*" } | Should -Be "PSCommandPath=$docFilePath;"; + } + } + + Context 'PSDocs automatic variables' { + BeforeAll { + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + Option = @{ + 'Configuration.author' = @{ name = 'unit-tester' } + 'Configuration.enabled' = 'faLse' + } + } + } + It '$PSDocs' { + $result = (Invoke-PSDocument @invokeParams -Name 'PSDocsVariable' | Out-String).Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries); + $result | Where-Object -FilterScript { $_ -like "TargetObject.Name=*" } | Should -Be 'TargetObject.Name=TestObject;'; + $result | Where-Object -FilterScript { $_ -like "Document.Metadata=*" } | Should -Be 'Document.Metadata=unit-tester;'; + $result | Where-Object -FilterScript { $_ -like "Document.Enabled=*" } | Should -BeNullOrEmpty; + } + It '$Document' { + $result = (Invoke-PSDocument @invokeParams -Name 'PSDocsDocumentVariable' | Out-String).Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries); + $result | Where-Object -FilterScript { $_ -like "Document.Title=*" } | Should -Be 'Document.Title=001;'; + $result | Where-Object -FilterScript { $_ -like "Document.Metadata=*" } | Should -Be 'Document.Metadata=002;'; + } + It '$LocalizedData' { + $result = (Invoke-PSDocument @invokeParams -Name 'PSDocsLocalizedDataVariable' | Out-String).Split([System.Environment]::NewLine, [System.StringSplitOptions]::RemoveEmptyEntries); + $result | Where-Object -FilterScript { $_ -like "LocalizedData.Key1=*" } | Should -Be 'LocalizedData.Key1=Value1;'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 new file mode 100644 index 00000000..a606ebee --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PSDocs.Warning.Tests.ps1 @@ -0,0 +1,45 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# Unit tests for the Warning keyword +# + +[CmdletBinding()] +param () + +BeforeAll{ +# Setup error handling +$ErrorActionPreference = 'Stop'; +Set-StrictMode -Version latest; + +# Setup tests paths +$rootPath = $PWD; +Import-Module (Join-Path -Path $rootPath -ChildPath out/modules/PSDocs) -Force; +$here = (Resolve-Path $PSScriptRoot).Path; +} +Describe 'PSDocs -- Warning keyword' -Tag Warning { + + + Context 'Markdown' { + BeforeAll{ + $docFilePath = Join-Path -Path $here -ChildPath 'FromFile.Keyword.Doc.ps1'; + $testObject = [PSCustomObject]@{ + Name = 'TestObject' + } + $invokeParams = @{ + Path = $docFilePath + InputObject = $testObject + PassThru = $True + } + } + It 'Should handle single line input' { + $result = Invoke-PSDocument @invokeParams -Name 'WarningSingleMarkdown'; + $result | Should -Match '\> \[\!WARNING\](\r|\n|\r\n)> This is a single line'; + } + It 'Should handle multiline input' { + $result = Invoke-PSDocument @invokeParams -Name 'WarningMultiMarkdown'; + $result | Should -Match '\> \[\!WARNING\](\r|\n|\r\n)> This is the first line\.(\r|\n|\r\n)> This is the second line\.'; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/PipelineTests.cs b/packages/psdocs/tests/PSDocs.Tests/PipelineTests.cs new file mode 100644 index 00000000..3b5beee4 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/PipelineTests.cs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Models; +using PSDocs.Pipeline; +using PSDocs.Runtime; + +namespace PSDocs +{ + public sealed class PipelineTests + { + [Fact] + public void GetDocumentBuilder() + { + var source = GetSource(); + var context = new RunspaceContext(new PipelineContext(GetOption(), null, null, null, null, null)); + HostHelper.ImportResource(source, context); + var actual = HostHelper.GetDocumentBuilder(context, source); + Assert.Equal(9, actual.Length); + } + + [Fact] + public void InvokePipeline() + { + var builder = PipelineBuilder.Invoke(GetSource(), GetOption(new string[] { "FromFileTest1" }), null, null); + var pipeline = builder.Build() as InvokePipeline; + var targetObject = PSObject.AsPSObject(new TestModel()); + + var actual = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Single(actual); + Assert.Equal("Test title", actual[0].Title); + Assert.Equal("Test1", actual[0].Metadata["test"]); + } + + [Fact] + public void InvokePipelineWithConvention() + { + var builder = PipelineBuilder.Invoke(GetSource(), GetOption(new string[] { "FromFileTest1" }), null, null); + var pipeline = builder.Build() as InvokePipeline; + var targetObject = PSObject.AsPSObject(new TestModel()); + + var actual = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Single(actual); + Assert.Equal("Test title", actual[0].Title); + Assert.Equal("Test1", actual[0].Metadata["test"]); + } + + [Fact] + public void InvokePipelineWithIf() + { + var builder = PipelineBuilder.Invoke(GetSource(), GetOption(new string[] { "WithIf" }), null, null); + var pipeline = builder.Build() as InvokePipeline; + var targetObject = PSObject.AsPSObject(new TestModel()); + + var actual1 = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Single(actual1); + Assert.Equal("Test", actual1[0].Metadata["Name"]); + + targetObject.Properties["Generator"].Value = "NotPSDocs"; + var actual2 = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Empty(actual2); + } + + [Fact] + public void InvokePipelineWithSelectors() + { + var builder = PipelineBuilder.Invoke(GetSourceWithSelectors(), GetOption(new string[] { "Selector.WithInputObject" }), null, null); + var pipeline = builder.Build() as InvokePipeline; + var targetObject = PSObject.AsPSObject(new TestModel()); + + var actual1 = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Single(actual1); + Assert.Equal("Test", actual1[0].Metadata["Name"]); + + targetObject.Properties["Generator"].Value = "NotPSDocs"; + var actual2 = pipeline.BuildDocument(new TargetObject(targetObject)); + Assert.Empty(actual2); + } + + private static OptionContext GetOption(string[] name = null) + { + var option = new PSDocumentOption(); + if (name != null && name.Length > 0) + option.Document.Include = name; + + option.Output.Culture = new string[] { "en-US" }; + return new OptionContext(option); + } + + private static Source[] GetSource() + { + var builder = new SourcePipelineBuilder(new HostContext(null, null)); + builder.Directory(GetSourcePath("FromFile.Doc.ps1")); + builder.Directory(GetSourcePath("Selectors.Doc.yaml")); + return builder.Build(); + } + + private static Source[] GetSourceWithSelectors() + { + var builder = new SourcePipelineBuilder(new HostContext(null, null)); + builder.Directory(GetSourcePath("FromFile.Selector.Doc.ps1")); + builder.Directory(GetSourcePath("Selectors.Doc.yaml")); + return builder.Build(); + } + + private static string GetSourcePath(string fileName) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/SelectorTests.cs b/packages/psdocs/tests/PSDocs.Tests/SelectorTests.cs new file mode 100644 index 00000000..fa194721 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/SelectorTests.cs @@ -0,0 +1,515 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using System.Linq; +using System.Management.Automation; +using PSDocs.Configuration; +using PSDocs.Definitions.Selectors; +using PSDocs.Pipeline; +using PSDocs.Runtime; + +namespace PSDocs +{ + public sealed class SelectorTests + { + [Fact] + public void ReadSelector() + { + var context = new RunspaceContext(new PipelineContext(GetOption(), null, null, null, null, null)); + var selector = HostHelper.GetSelector(context, GetSource()).ToArray(); + Assert.NotNull(selector); + Assert.Equal(31, selector.Length); + + Assert.Equal("BasicSelector", selector[0].Name); + Assert.Equal("YamlAllOf", selector[4].Name); + } + + #region Conditions + + [Fact] + public void ExistsExpression() + { + var existsTrue = GetSelectorVisitor("YamlExistsTrue"); + var existsFalse = GetSelectorVisitor("YamlExistsFalse"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "notValue", value: 3)); + var actual3 = GetObject((name: "value", value: null)); + + Assert.True(existsTrue.Match(actual1)); + Assert.False(existsTrue.Match(actual2)); + Assert.True(existsTrue.Match(actual3)); + + Assert.False(existsFalse.Match(actual1)); + Assert.True(existsFalse.Match(actual2)); + Assert.False(existsFalse.Match(actual3)); + } + + [Fact] + public void EqualsExpression() + { + var equals = GetSelectorVisitor("YamlEquals"); + var actual1 = GetObject( + (name: "ValueString", value: "abc"), + (name: "ValueInt", value: 123), + (name: "ValueBool", value: true) + ); + var actual2 = GetObject( + (name: "ValueString", value: "efg"), + (name: "ValueInt", value: 123), + (name: "ValueBool", value: true) + ); + var actual3 = GetObject( + (name: "ValueString", value: "abc"), + (name: "ValueInt", value: 456), + (name: "ValueBool", value: true) + ); + var actual4 = GetObject( + (name: "ValueString", value: "abc"), + (name: "ValueInt", value: 123), + (name: "ValueBool", value: false) + ); + + Assert.True(equals.Match(actual1)); + Assert.False(equals.Match(actual2)); + Assert.False(equals.Match(actual3)); + Assert.False(equals.Match(actual4)); + } + + [Fact] + public void NotEqualsExpression() + { + var notEquals = GetSelectorVisitor("YamlNotEquals"); + var actual1 = GetObject( + (name: "ValueString", value: "efg"), + (name: "ValueInt", value: 456), + (name: "ValueBool", value: false) + ); + var actual2 = GetObject( + (name: "ValueString", value: "abc"), + (name: "ValueInt", value: 456), + (name: "ValueBool", value: false) + ); + var actual3 = GetObject( + (name: "ValueString", value: "efg"), + (name: "ValueInt", value: 123), + (name: "ValueBool", value: false) + ); + var actual4 = GetObject( + (name: "ValueString", value: "efg"), + (name: "ValueInt", value: 456), + (name: "ValueBool", value: true) + ); + + Assert.True(notEquals.Match(actual1)); + Assert.False(notEquals.Match(actual2)); + Assert.False(notEquals.Match(actual3)); + Assert.False(notEquals.Match(actual4)); + } + + [Fact] + public void HasValueExpression() + { + var hasValueTrue = GetSelectorVisitor("YamlHasValueTrue"); + var hasValueFalse = GetSelectorVisitor("YamlHasValueFalse"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "notValue", value: 3)); + var actual3 = GetObject((name: "value", value: null)); + + Assert.True(hasValueTrue.Match(actual1)); + Assert.False(hasValueTrue.Match(actual2)); + Assert.False(hasValueTrue.Match(actual3)); + + Assert.False(hasValueFalse.Match(actual1)); + Assert.True(hasValueFalse.Match(actual2)); + Assert.True(hasValueFalse.Match(actual3)); + } + + [Fact] + public void MatchExpression() + { + var match = GetSelectorVisitor("YamlMatch"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: 0)); + var actual5 = GetObject(); + + Assert.True(match.Match(actual1)); + Assert.True(match.Match(actual2)); + Assert.False(match.Match(actual3)); + Assert.False(match.Match(actual4)); + Assert.False(match.Match(actual5)); + } + + [Fact] + public void NotMatchExpression() + { + var notMatch = GetSelectorVisitor("YamlNotMatch"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: 0)); + + Assert.False(notMatch.Match(actual1)); + Assert.False(notMatch.Match(actual2)); + Assert.True(notMatch.Match(actual3)); + Assert.True(notMatch.Match(actual4)); + } + + [Fact] + public void InExpression() + { + var @in = GetSelectorVisitor("YamlIn"); + var actual1 = GetObject((name: "value", value: new string[] { "Value1" })); + var actual2 = GetObject((name: "value", value: new string[] { "Value2" })); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(@in.Match(actual1)); + Assert.True(@in.Match(actual2)); + Assert.False(@in.Match(actual3)); + Assert.False(@in.Match(actual4)); + Assert.False(@in.Match(actual5)); + Assert.False(@in.Match(actual6)); + } + + [Fact] + public void NotInExpression() + { + var notIn = GetSelectorVisitor("YamlNotIn"); + var actual1 = GetObject((name: "value", value: new string[] { "Value1" })); + var actual2 = GetObject((name: "value", value: new string[] { "Value2" })); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + + Assert.False(notIn.Match(actual1)); + Assert.False(notIn.Match(actual2)); + Assert.True(notIn.Match(actual3)); + Assert.True(notIn.Match(actual4)); + Assert.True(notIn.Match(actual5)); + } + + [Fact] + public void LessExpression() + { + var less = GetSelectorVisitor("YamlLess"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject((name: "value", value: 2)); + var actual7 = GetObject((name: "value", value: -1)); + + Assert.False(less.Match(actual1)); + Assert.False(less.Match(actual2)); + Assert.True(less.Match(actual3)); + Assert.True(less.Match(actual4)); + Assert.True(less.Match(actual5)); + Assert.True(less.Match(actual6)); + Assert.True(less.Match(actual7)); + } + + [Fact] + public void LessOrEqualsExpression() + { + var lessOrEquals = GetSelectorVisitor("YamlLessOrEquals"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject((name: "value", value: 2)); + var actual7 = GetObject((name: "value", value: -1)); + + Assert.True(lessOrEquals.Match(actual1)); + Assert.False(lessOrEquals.Match(actual2)); + Assert.True(lessOrEquals.Match(actual3)); + Assert.True(lessOrEquals.Match(actual4)); + Assert.True(lessOrEquals.Match(actual5)); + Assert.True(lessOrEquals.Match(actual6)); + Assert.True(lessOrEquals.Match(actual7)); + } + + [Fact] + public void GreaterExpression() + { + var greater = GetSelectorVisitor("YamlGreater"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject((name: "value", value: 2)); + var actual7 = GetObject((name: "value", value: -1)); + + Assert.False(greater.Match(actual1)); + Assert.True(greater.Match(actual2)); + Assert.False(greater.Match(actual3)); + Assert.False(greater.Match(actual4)); + Assert.False(greater.Match(actual5)); + Assert.False(greater.Match(actual6)); + Assert.False(greater.Match(actual7)); + } + + [Fact] + public void GreaterOrEqualsExpression() + { + var greaterOrEquals = GetSelectorVisitor("YamlGreaterOrEquals"); + var actual1 = GetObject((name: "value", value: 3)); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { "Value3" })); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject((name: "value", value: 2)); + var actual7 = GetObject((name: "value", value: -1)); + + Assert.True(greaterOrEquals.Match(actual1)); + Assert.True(greaterOrEquals.Match(actual2)); + Assert.False(greaterOrEquals.Match(actual3)); + Assert.False(greaterOrEquals.Match(actual4)); + Assert.False(greaterOrEquals.Match(actual5)); + Assert.False(greaterOrEquals.Match(actual6)); + Assert.False(greaterOrEquals.Match(actual7)); + } + + [Fact] + public void StartsWithExpression() + { + var startsWith = GetSelectorVisitor("YamlStartsWith"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(startsWith.Match(actual1)); + Assert.True(startsWith.Match(actual2)); + Assert.False(startsWith.Match(actual3)); + Assert.False(startsWith.Match(actual4)); + Assert.False(startsWith.Match(actual5)); + Assert.False(startsWith.Match(actual6)); + } + + [Fact] + public void EndsWithExpression() + { + var endsWith = GetSelectorVisitor("YamlEndsWith"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "efg")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(endsWith.Match(actual1)); + Assert.True(endsWith.Match(actual2)); + Assert.False(endsWith.Match(actual3)); + Assert.False(endsWith.Match(actual4)); + Assert.False(endsWith.Match(actual5)); + Assert.False(endsWith.Match(actual6)); + } + + [Fact] + public void ContainsExpression() + { + var contains = GetSelectorVisitor("YamlContains"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "bcd")); + var actual3 = GetObject((name: "value", value: "hij")); + var actual4 = GetObject((name: "value", value: new string[] { })); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + Assert.True(contains.Match(actual1)); + Assert.True(contains.Match(actual2)); + Assert.False(contains.Match(actual3)); + Assert.False(contains.Match(actual4)); + Assert.False(contains.Match(actual5)); + Assert.False(contains.Match(actual6)); + } + + [Fact] + public void IsStringExpression() + { + var isStringTrue = GetSelectorVisitor("YamlIsStringTrue"); + var isStringFalse = GetSelectorVisitor("YamlIsStringFalse"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: 4)); + var actual3 = GetObject((name: "value", value: new string[] { })); + var actual4 = GetObject((name: "value", value: null)); + var actual5 = GetObject(); + + // isString: true + Assert.True(isStringTrue.Match(actual1)); + Assert.False(isStringTrue.Match(actual2)); + Assert.False(isStringTrue.Match(actual3)); + Assert.False(isStringTrue.Match(actual4)); + Assert.False(isStringTrue.Match(actual5)); + + // isString: false + Assert.False(isStringFalse.Match(actual1)); + Assert.True(isStringFalse.Match(actual2)); + Assert.True(isStringFalse.Match(actual3)); + Assert.True(isStringFalse.Match(actual4)); + Assert.False(isStringFalse.Match(actual5)); + } + + [Fact] + public void IsLowerExpression() + { + var isLowerTrue = GetSelectorVisitor("YamlIsLowerTrue"); + var isLowerFalse = GetSelectorVisitor("YamlIsLowerFalse"); + var actual1 = GetObject((name: "value", value: "abc")); + var actual2 = GetObject((name: "value", value: "aBc")); + var actual3 = GetObject((name: "value", value: "a-b-c")); + var actual4 = GetObject((name: "value", value: 4)); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + // isLower: true + Assert.True(isLowerTrue.Match(actual1)); + Assert.False(isLowerTrue.Match(actual2)); + Assert.True(isLowerTrue.Match(actual3)); + Assert.False(isLowerTrue.Match(actual4)); + Assert.False(isLowerTrue.Match(actual5)); + Assert.False(isLowerTrue.Match(actual6)); + + // isLower: false + Assert.False(isLowerFalse.Match(actual1)); + Assert.True(isLowerFalse.Match(actual2)); + Assert.False(isLowerFalse.Match(actual3)); + Assert.True(isLowerFalse.Match(actual4)); + Assert.True(isLowerFalse.Match(actual5)); + Assert.False(isLowerTrue.Match(actual6)); + } + + [Fact] + public void IsUpperExpression() + { + var isUpperTrue = GetSelectorVisitor("YamlIsUpperTrue"); + var isUpperFalse = GetSelectorVisitor("YamlIsUpperFalse"); + var actual1 = GetObject((name: "value", value: "ABC")); + var actual2 = GetObject((name: "value", value: "aBc")); + var actual3 = GetObject((name: "value", value: "A-B-C")); + var actual4 = GetObject((name: "value", value: 4)); + var actual5 = GetObject((name: "value", value: null)); + var actual6 = GetObject(); + + // isUpper: true + Assert.True(isUpperTrue.Match(actual1)); + Assert.False(isUpperTrue.Match(actual2)); + Assert.True(isUpperTrue.Match(actual3)); + Assert.False(isUpperTrue.Match(actual4)); + Assert.False(isUpperTrue.Match(actual5)); + Assert.False(isUpperTrue.Match(actual6)); + + // isUpper: false + Assert.False(isUpperFalse.Match(actual1)); + Assert.True(isUpperFalse.Match(actual2)); + Assert.False(isUpperFalse.Match(actual3)); + Assert.True(isUpperFalse.Match(actual4)); + Assert.True(isUpperFalse.Match(actual5)); + Assert.False(isUpperFalse.Match(actual6)); + } + + #endregion Conditions + + #region Operators + + [Fact] + public void AllOf() + { + var allOf = GetSelectorVisitor("YamlAllOf"); + var actual1 = GetObject((name: "Name", value: "Name1")); + var actual2 = GetObject((name: "AlternateName", value: "Name2")); + var actual3 = GetObject((name: "Name", value: "Name1"), (name: "AlternateName", value: "Name2")); + var actual4 = GetObject((name: "OtherName", value: "Name3")); + + Assert.False(allOf.Match(actual1)); + Assert.False(allOf.Match(actual2)); + Assert.True(allOf.Match(actual3)); + Assert.False(allOf.Match(actual4)); + } + + [Fact] + public void AnyOf() + { + var allOf = GetSelectorVisitor("YamlAnyOf"); + var actual1 = GetObject((name: "Name", value: "Name1")); + var actual2 = GetObject((name: "AlternateName", value: "Name2")); + var actual3 = GetObject((name: "Name", value: "Name1"), (name: "AlternateName", value: "Name2")); + var actual4 = GetObject((name: "OtherName", value: "Name3")); + + Assert.True(allOf.Match(actual1)); + Assert.True(allOf.Match(actual2)); + Assert.True(allOf.Match(actual3)); + Assert.False(allOf.Match(actual4)); + } + + [Fact] + public void Not() + { + var allOf = GetSelectorVisitor("YamlNot"); + var actual1 = GetObject((name: "Name", value: "Name1")); + var actual2 = GetObject((name: "AlternateName", value: "Name2")); + var actual3 = GetObject((name: "Name", value: "Name1"), (name: "AlternateName", value: "Name2")); + var actual4 = GetObject((name: "OtherName", value: "Name3")); + + Assert.False(allOf.Match(actual1)); + Assert.False(allOf.Match(actual2)); + Assert.False(allOf.Match(actual3)); + Assert.True(allOf.Match(actual4)); + } + + #endregion Operators + + #region Helper methods + + private static OptionContext GetOption(string[] name = null) + { + var option = new PSDocumentOption(); + if (name != null && name.Length > 0) + option.Document.Include = name; + + option.Output.Culture = new string[] { "en-US" }; + return new OptionContext(option); + } + + private static Source[] GetSource() + { + var builder = new SourcePipelineBuilder(new HostContext(null, null)); + builder.Directory(GetSourcePath("Selectors.Doc.yaml")); + return builder.Build(); + } + + private static PSObject GetObject(params (string name, object value)[] properties) + { + var result = new PSObject(); + for (var i = 0; properties != null && i < properties.Length; i++) + result.Properties.Add(new PSNoteProperty(properties[i].Item1, properties[i].Item2)); + + return result; + } + + private static SelectorVisitor GetSelectorVisitor(string name) + { + var context = new PipelineContext(GetOption(), null, null, null, null, null); + var selector = HostHelper.GetSelector(new RunspaceContext(context), GetSource()).ToArray(); + return new SelectorVisitor(name, selector.FirstOrDefault(s => s.Name == name).Spec.If); + } + + private static string GetSourcePath(string fileName) + { + return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, fileName); + } + + #endregion Helper methods + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/Selectors.Doc.yaml b/packages/psdocs/tests/PSDocs.Tests/Selectors.Doc.yaml new file mode 100644 index 00000000..9c8bdf84 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/Selectors.Doc.yaml @@ -0,0 +1,400 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Selectors for unit testing + +--- +# Synopsis: A selector to match basic objects +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: BasicSelector +spec: + if: + allOf: + - field: Name + equals: TargetObject1 + - field: Value + equals: value1 + +--- +# Synopsis: A selector to match objects using a specific JSON schema +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: SelectJsonSchema +spec: + if: + field: '$schema' + match: '^(https{0,1}://schema\.management\.azure\.com/schemas/.*/deploymentTemplate\.json)$' + +--- +# Synopsis: A selector to match object with a field +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: HasCustomValueField +spec: + if: + field: 'CustomValue' + exists: true + +--- +# Synopsis: Test AnyOf +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlAnyOf +spec: + if: + anyOf: + - field: 'AlternateName' + exists: true + - field: 'Name' + exists: true + +--- +# Synopsis: Test AllOf +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlAllOf +spec: + if: + allOf: + - field: 'AlternateName' + exists: true + - field: 'Name' + exists: true + +--- +# Synopsis: Test Not +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlNot +spec: + if: + not: + anyOf: + - field: 'AlternateName' + exists: true + - field: 'Name' + exists: true + +--- +# Synopsis: Test custom value is in +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlCustomValueIn +spec: + if: + field: 'CustomValue' + in: + - 'Value1' + - 'Value2' + +--- +# Synopsis: Test exists true +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlExistsTrue +spec: + if: + field: 'Value' + exists: true + +--- +# Synopsis: Test exists false +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlExistsFalse +spec: + if: + field: 'Value' + exists: false + +--- +# Synopsis: Test equals +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlEquals +spec: + if: + allOf: + - field: 'ValueString' + equals: 'abc' + - field: 'ValueInt' + equals: 123 + - field: 'ValueBool' + equals: true + +--- +# Synopsis: Test notEquals +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlNotEquals +spec: + if: + allOf: + - field: 'ValueString' + notEquals: 'abc' + - field: 'ValueInt' + notEquals: 123 + - field: 'ValueBool' + notEquals: true + +--- +# Synopsis: Test hasValue true +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlHasValueTrue +spec: + if: + field: 'Value' + hasValue: true + +--- +# Synopsis: Test hasValue false +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlHasValueFalse +spec: + if: + field: 'Value' + hasValue: false + +--- +# Synopsis: Test match +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlMatch +spec: + if: + field: 'Value' + match: '^(abc|efg)$' + +--- +# Synopsis: Test notMatch +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlNotMatch +spec: + if: + field: 'Value' + notMatch: '^(abc|efg)$' + +--- +# Synopsis: Test in +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIn +spec: + if: + field: 'Value' + in: + - 'Value1' + - 'Value2' + +--- +# Synopsis: Test notIn +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlNotIn +spec: + if: + field: 'Value' + notIn: + - 'Value1' + - 'Value2' + +--- +# Synopsis: Test less +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlLess +spec: + if: + field: 'Value' + less: 3 + +--- +# Synopsis: Test lessOrEquals +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlLessOrEquals +spec: + if: + field: 'Value' + lessOrEquals: 3 + +--- +# Synopsis: Test greater +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlGreater +spec: + if: + field: 'Value' + greater: 3 + +--- +# Synopsis: Test greaterOrEquals +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlGreaterOrEquals +spec: + if: + field: 'Value' + greaterOrEquals: 3 + +--- +# Synopsis: Test startsWith +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlStartsWith +spec: + if: + allOf: + - anyOf: + - field: 'Value' + startsWith: 'a' + - field: 'Value' + startsWith: 'e' + - field: 'Value' + startsWith: + - 'a' + - 'e' + +--- +# Synopsis: Test endsWith +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlEndsWith +spec: + if: + allOf: + - anyOf: + - field: 'Value' + endsWith: 'c' + - field: 'Value' + endsWith: 'g' + - field: 'Value' + endsWith: + - 'c' + - 'g' + +--- +# Synopsis: Test contains +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlContains +spec: + if: + allOf: + - anyOf: + - field: 'Value' + contains: 'ab' + - field: 'Value' + contains: 'bc' + - field: 'Value' + contains: + - 'ab' + - 'bc' + +--- +# Synopsis: Test isString +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsStringTrue +spec: + if: + field: 'Value' + isString: true + +--- +# Synopsis: Test isString +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsStringFalse +spec: + if: + field: 'Value' + isString: false + +--- +# Synopsis: Test isLower +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsLowerTrue +spec: + if: + field: 'Value' + isLower: true + +--- +# Synopsis: Test isLower +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsLowerFalse +spec: + if: + field: 'Value' + isLower: false + + +--- +# Synopsis: Test isUpper +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsUpperTrue +spec: + if: + field: 'Value' + isUpper: true + +--- +# Synopsis: Test isUpper +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: YamlIsUpperFalse +spec: + if: + field: 'Value' + isUpper: false + +--- +# Synopsis: A selector to match basic objects +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: GeneratorSelector +spec: + if: + allOf: + - field: generator + equals: PSDocs diff --git a/packages/psdocs/tests/PSDocs.Tests/StringContentTests.cs b/packages/psdocs/tests/PSDocs.Tests/StringContentTests.cs new file mode 100644 index 00000000..d5eda143 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/StringContentTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSDocs.Runtime; + +namespace PSDocs +{ + public sealed class StringContentTests + { + [Fact] + public void ReadLines() + { + var lines = new StringContent(GetTestString1()).ReadLines(); + Assert.Equal("Line 1", lines[0]); + Assert.Equal("Line 2", lines[1]); + Assert.Equal("Line 3", lines[2]); + + lines = new StringContent(GetTestString2()).ReadLines(); + Assert.Equal("Line1", lines[0]); + Assert.Equal("Line2", lines[1]); + Assert.Equal(" Line3", lines[2]); + + lines = new StringContent(GetTestString3()).ReadLines(); + Assert.Equal("Line 1", lines[0]); + Assert.Equal("Line 2", lines[1]); + Assert.Equal(string.Empty, lines[2]); + Assert.Equal("Line 3", lines[3]); + } + + private static string GetTestString1() + { + return @" +Line 1 +Line 2 +Line 3 +"; + } + + private static string GetTestString2() + { + return @" + Line1 + Line2 + Line3 +"; + } + + private static string GetTestString3() + { + return @" + Line 1 + Line 2 + + Line 3 +"; + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestCommandRuntime.cs b/packages/psdocs/tests/PSDocs.Tests/TestCommandRuntime.cs new file mode 100644 index 00000000..08de1621 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestCommandRuntime.cs @@ -0,0 +1,136 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; +using System.Collections.Generic; +using System.Management.Automation; +using System.Management.Automation.Host; + +namespace PSDocs +{ + internal sealed class TestCommandRuntime : ICommandRuntime2 + { + public readonly List Error; + public readonly List Warning; + public readonly List Verbose; + public readonly List Information; + public readonly List Output; + + public TestCommandRuntime() + { + Error = new List(); + Warning = new List(); + Verbose = new List(); + Information = new List(); + Output = new List(); + } + + PSTransactionContext ICommandRuntime.CurrentPSTransaction => throw new System.NotImplementedException(); + + PSHost ICommandRuntime.Host => throw new System.NotImplementedException(); + + bool ICommandRuntime2.ShouldContinue(string query, string caption, bool hasSecurityImpact, ref bool yesToAll, ref bool noToAll) + { + return true; + } + + bool ICommandRuntime.ShouldContinue(string query, string caption) + { + return true; + } + + bool ICommandRuntime.ShouldContinue(string query, string caption, ref bool yesToAll, ref bool noToAll) + { + return true; + } + + bool ICommandRuntime.ShouldProcess(string target) + { + return true; + } + + bool ICommandRuntime.ShouldProcess(string target, string action) + { + return true; + } + + bool ICommandRuntime.ShouldProcess(string verboseDescription, string verboseWarning, string caption) + { + return true; + } + + bool ICommandRuntime.ShouldProcess(string verboseDescription, string verboseWarning, string caption, out ShouldProcessReason shouldProcessReason) + { + shouldProcessReason = ShouldProcessReason.None; + return true; + } + + void ICommandRuntime.ThrowTerminatingError(ErrorRecord errorRecord) + { + + } + + bool ICommandRuntime.TransactionAvailable() + { + return false; + } + + void ICommandRuntime.WriteCommandDetail(string text) + { + + } + + void ICommandRuntime.WriteDebug(string text) + { + + } + + void ICommandRuntime.WriteError(ErrorRecord errorRecord) + { + Error.Add(errorRecord); + } + + void ICommandRuntime2.WriteInformation(InformationRecord informationRecord) + { + Information.Add(informationRecord); + } + + void ICommandRuntime.WriteObject(object sendToPipeline) + { + Output.Add(sendToPipeline); + } + + void ICommandRuntime.WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (enumerateCollection && sendToPipeline is IEnumerable collection) + { + foreach (var o in collection) + { + Output.Add(o); + } + } + else + Output.Add(sendToPipeline); + } + + void ICommandRuntime.WriteProgress(long sourceId, ProgressRecord progressRecord) + { + + } + + void ICommandRuntime.WriteProgress(ProgressRecord progressRecord) + { + + } + + void ICommandRuntime.WriteVerbose(string text) + { + Verbose.Add(text); + } + + void ICommandRuntime.WriteWarning(string text) + { + Warning.Add(text); + } + } +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/TestModule.psd1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/TestModule.psd1 new file mode 100644 index 00000000..3860a2ee --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/TestModule.psd1 @@ -0,0 +1,130 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# +# A module for testing +# +@{ + +# Script module or binary module file associated with this manifest. +# RootModule = '' + +# Version number of this module. +ModuleVersion = '0.0.1' + +# Supported PSEditions +CompatiblePSEditions = 'Core', 'Desktop' + +# ID used to uniquely identify this module +GUID = '9190b71c-7959-4c86-a420-2d78c62ecbba' + +# Author of this module +Author = 'Bernie White' + +# Company or vendor of this module +CompanyName = 'Bernie White' + +# Copyright statement for this module +Copyright = '(c) Bernie White. All rights reserved.' + +# Description of the functionality provided by this module +# Description = '' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +# TypesToProcess = @() + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. +FunctionsToExport = @() + +# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. +CmdletsToExport = @() + +# Variables to export from this module +VariablesToExport = '*' + +# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. +AliasesToExport = @() + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('PSDocs-documents') + + # A URL to the license for this module. + # LicenseUri = '' + + # A URL to the main website for this project. + # ProjectUri = '' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + # ReleaseNotes = '' + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Selector.Doc.yaml b/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Selector.Doc.yaml new file mode 100644 index 00000000..0e3b2f22 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Selector.Doc.yaml @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +--- +# Synopsis: A selector for unit testing. +apiVersion: github.com/microsoft/PSDocs/v1 +kind: Selector +metadata: + name: AlwaysTrue +spec: + if: + field: '__not_value__' + exists: false diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Test.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Test.Doc.ps1 new file mode 100644 index 00000000..75487696 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/docs/Test.Doc.ps1 @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Document 'TestDocument1' { + "Culture=$($LocalizedData.Culture)"; +} + +Document 'TestDocument2' -With 'AlwaysTrue' { + +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/en-AU/PSDocs-strings.psd1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-AU/PSDocs-strings.psd1 new file mode 100644 index 00000000..b03e6b74 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-AU/PSDocs-strings.psd1 @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + Culture = 'en-AU' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/en-US/PSDocs-strings.psd1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-US/PSDocs-strings.psd1 new file mode 100644 index 00000000..72f45e9b --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-US/PSDocs-strings.psd1 @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + Culture = 'en-US' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/en-ZZ/PSDocs-strings.psd1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-ZZ/PSDocs-strings.psd1 new file mode 100644 index 00000000..400e9480 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/en-ZZ/PSDocs-strings.psd1 @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + # No strings +} diff --git a/packages/psdocs/tests/PSDocs.Tests/TestModule/en/PSDocs-strings.psd1 b/packages/psdocs/tests/PSDocs.Tests/TestModule/en/PSDocs-strings.psd1 new file mode 100644 index 00000000..1a111b43 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/TestModule/en/PSDocs-strings.psd1 @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + Culture = 'en' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/Usings.cs b/packages/psdocs/tests/PSDocs.Tests/Usings.cs new file mode 100644 index 00000000..8c07c6cf --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/Usings.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +global using Xunit; diff --git a/packages/psdocs/tests/PSDocs.Tests/WithError.Doc.ps1 b/packages/psdocs/tests/PSDocs.Tests/WithError.Doc.ps1 new file mode 100644 index 00000000..4b818fca --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/WithError.Doc.ps1 @@ -0,0 +1,16 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +document 'InvalidCommand' { + New-PSDocsInvalidCommand; +} + +document 'InvalidCommandWithSection' { + Section 'Invalid' { + New-PSDocsInvalidCommand; + } +} + +document 'WithWriteError' { + Write-Error -Message 'Verify Write-Error is raised as an exception'; +} diff --git a/packages/psdocs/tests/PSDocs.Tests/en-AU/IncludeFile3.md b/packages/psdocs/tests/PSDocs.Tests/en-AU/IncludeFile3.md new file mode 100644 index 00000000..90436372 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/en-AU/IncludeFile3.md @@ -0,0 +1 @@ +This is en-AU. \ No newline at end of file diff --git a/packages/psdocs/tests/PSDocs.Tests/en-US/IncludeFile3.md b/packages/psdocs/tests/PSDocs.Tests/en-US/IncludeFile3.md new file mode 100644 index 00000000..bcb8d4ae --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/en-US/IncludeFile3.md @@ -0,0 +1 @@ +This is en-US. \ No newline at end of file diff --git a/packages/psdocs/tests/PSDocs.Tests/en-US/PSDocs-strings.psd1 b/packages/psdocs/tests/PSDocs.Tests/en-US/PSDocs-strings.psd1 new file mode 100644 index 00000000..048410e6 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/en-US/PSDocs-strings.psd1 @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +@{ + Key1 = 'Value1' +} diff --git a/packages/psdocs/tests/PSDocs.Tests/psdocs.yml b/packages/psdocs/tests/PSDocs.Tests/psdocs.yml new file mode 100644 index 00000000..a39fedb3 --- /dev/null +++ b/packages/psdocs/tests/PSDocs.Tests/psdocs.yml @@ -0,0 +1,2 @@ + +generator: PSDocs diff --git a/packages/vscode-extension/.eslintrc.json b/packages/vscode-extension/.eslintrc.json new file mode 100644 index 00000000..f9b22b79 --- /dev/null +++ b/packages/vscode-extension/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "root": true, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 6, + "sourceType": "module" + }, + "plugins": [ + "@typescript-eslint" + ], + "rules": { + "@typescript-eslint/naming-convention": "warn", + "@typescript-eslint/semi": "warn", + "curly": "warn", + "eqeqeq": "warn", + "no-throw-literal": "warn", + "semi": "off" + }, + "ignorePatterns": [ + "out", + "dist", + "**/*.d.ts" + ] +} diff --git a/packages/vscode-extension/.github/ISSUE_TEMPLATE/bug_report.md b/packages/vscode-extension/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..cb65fb85 --- /dev/null +++ b/packages/vscode-extension/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Report errors or an unexpected issue +--- + +**Description of the issue** + +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the issue: + +```powershell + +``` + +**Expected behaviour** + +A clear and concise description of what you expected to happen. + +**Error output** + +Capture any error messages and or terminal output. + +```text + +``` + +**Extension version:** + +- Version: **[e.g. 1.0.0]** + +**PSRule module version:** + +Captured output from `Get-InstalledModule PSRule | Format-List Name,Version`: + +```text + +``` + +**Additional context** + +Add any other context about the problem here. diff --git a/packages/vscode-extension/.github/ISSUE_TEMPLATE/feature_request.md b/packages/vscode-extension/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..f64b1078 --- /dev/null +++ b/packages/vscode-extension/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea +--- + +**Is your feature request related to a problem? Please describe.** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** + +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** + +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** + +Add any other context or screenshots about the feature request here. diff --git a/packages/vscode-extension/.github/PULL_REQUEST_TEMPLATE.md b/packages/vscode-extension/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..21ba9a53 --- /dev/null +++ b/packages/vscode-extension/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +## PR Summary + + + +## PR Checklist + +- [ ] PR has a meaningful title +- [ ] Summarized changes +- [ ] Change is not breaking +- [ ] This PR is ready to merge and is not **Work in Progress** +- **Code changes** + - [ ] Link to a filed issue + - [ ] [Change log](https://github.com/Microsoft/PSDocs-vscode/blob/main/CHANGELOG.md) has been updated with change under unreleased section \ No newline at end of file diff --git a/packages/vscode-extension/.github/dependabot.yml b/packages/vscode-extension/.github/dependabot.yml new file mode 100644 index 00000000..7d22984e --- /dev/null +++ b/packages/vscode-extension/.github/dependabot.yml @@ -0,0 +1,17 @@ +# +# Dependabot configuration +# + +# Please see the documentation for all configuration options: +# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + +# Maintain dependencies for npm +- package-ecosystem: 'npm' + directory: '/' + schedule: + interval: 'monthly' + labels: + - 'dependencies' diff --git a/packages/vscode-extension/.gitignore b/packages/vscode-extension/.gitignore new file mode 100644 index 00000000..e51bdcc1 --- /dev/null +++ b/packages/vscode-extension/.gitignore @@ -0,0 +1,6 @@ +out +dist +node_modules +.vscode-test/ +*.vsix +UPDATE.md diff --git a/packages/vscode-extension/.vscode/extensions.json b/packages/vscode-extension/.vscode/extensions.json new file mode 100644 index 00000000..991a08c3 --- /dev/null +++ b/packages/vscode-extension/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + // See http://go.microsoft.com/fwlink/?LinkId=827846 + // for the documentation about the extensions.json format + "recommendations": [ + "ms-vscode.vscode-typescript-tslint-plugin", + "esbenp.prettier-vscode" + ] +} diff --git a/packages/vscode-extension/.vscode/launch.json b/packages/vscode-extension/.vscode/launch.json new file mode 100644 index 00000000..4b0285cf --- /dev/null +++ b/packages/vscode-extension/.vscode/launch.json @@ -0,0 +1,35 @@ +// A launch configuration that compiles the extension and then opens it inside a new window +// Use IntelliSense to learn about possible attributes. +// Hover to view descriptions of existing attributes. +// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Run Extension", + "type": "extensionHost", + "request": "launch", + "args": [ + "--extensionDevelopmentPath=${workspaceFolder}" + ], + "outFiles": [ + "${workspaceFolder}/out/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + }, + { + "name": "Extension Tests", + "type": "extensionHost", + "request": "launch", + "args": [ + "--disable-extensions", + "--extensionDevelopmentPath=${workspaceFolder}", + "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" + ], + "outFiles": [ + "${workspaceFolder}/out/test/**/*.js" + ], + "preLaunchTask": "${defaultBuildTask}" + } + ] +} diff --git a/packages/vscode-extension/.vscode/settings.json b/packages/vscode-extension/.vscode/settings.json new file mode 100644 index 00000000..4b3e6bfe --- /dev/null +++ b/packages/vscode-extension/.vscode/settings.json @@ -0,0 +1,40 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + //"powershell.integratedConsole.suppressStartupBanner": true, + //"terminal.integrated.automationShell.windows": "cmd.exe" + "files.associations": { + "**/.azure-pipelines/*.yaml": "azure-pipelines", + "**/.azure-pipelines/jobs/*.yaml": "azure-pipelines" + }, + "editor.insertSpaces": true, + "editor.tabSize": 4, + "[yaml]": { + "editor.tabSize": 2 + }, + "[markdown]": { + "editor.tabSize": 2 + }, + "[json]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescript]": { + "editor.formatOnSave": true, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "cSpell.words": [ + "pwsh" + ] +} \ No newline at end of file diff --git a/packages/vscode-extension/.vscode/tasks.json b/packages/vscode-extension/.vscode/tasks.json new file mode 100644 index 00000000..5a0a9257 --- /dev/null +++ b/packages/vscode-extension/.vscode/tasks.json @@ -0,0 +1,31 @@ +// See https://go.microsoft.com/fwlink/?LinkId=733558 +// for the documentation about the tasks.json format +{ + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "watch", + "problemMatcher": "$tsc-watch", + "isBackground": true, + "presentation": { + "reveal": "never" + }, + "group": { + "kind": "build", + "isDefault": true + } + }, + { + "label": "Compile", + "type": "npm", + "script": "compile", + "presentation": { + "focus": false, + "panel": "dedicated", + "clear": true + }, + "problemMatcher": ["$tsc"] + } + ] +} diff --git a/packages/vscode-extension/.vscodeignore b/packages/vscode-extension/.vscodeignore new file mode 100644 index 00000000..fbd88243 --- /dev/null +++ b/packages/vscode-extension/.vscodeignore @@ -0,0 +1,12 @@ +.vscode/** +.vscode-test/** +out/test/** + +src/** +.gitignore +.yarnrc +vsc-extension-quickstart.md +**/tsconfig.json +**/.eslintrc.json +**/*.map +**/*.ts diff --git a/packages/vscode-extension/CHANGELOG.md b/packages/vscode-extension/CHANGELOG.md new file mode 100644 index 00000000..7183bf52 --- /dev/null +++ b/packages/vscode-extension/CHANGELOG.md @@ -0,0 +1,82 @@ +# Change log + +All notable changes to this extension will be documented in this file. +This extension is available in two release channels for Visual Studio Code from the Visual Studio Marketplace. + + - Uses [semantic versioning](http://semver.org/) to declare changes. +Continue reading to see the changes included in the latest version. + +## Unreleased +Updated Pipeline step to remove GitHub release. + +## v0.3.3 (27 May 2024) +- "@types/node": "^20.11.5" +- "@types/vscode": "^1.89.0" +- "esbuild": "^0.19.11" +- "eslint": "^8.56.0" +- "@vscode/vsce": "^2.22.0" +- "glob": "^10.3.10" +- "mocha": "^10.2.0" +- "typescript": "^5.3.3" + +## v0.3.2 (17 Jan 2024) +Engineering updates +- "@types/node": "^20.11.5" +- "@types/vscode": "^1.85.0" +- "esbuild": "^0.19.11" +- "eslint": "^8.56.0" +- "@vscode/vsce": "^2.22.0" + +## v0.3.1 +Jan 2024 Engineering updates +- "@types/vscode": "^1.85.0" +- "@types/glob": "^8.1.0" +- "@types/mocha": "^10.0.6" +- "@types/node": "^20.11.3" +- "eslint": "^8.56.0" +- "esbuild": "^0.19.11" +- "@typescript-eslint/eslint-plugin": "^6.19.0" +- "@typescript-eslint/parser": "^6.19.0" +- "glob": "^10.3.10" +- "mocha": "^10.2.0" +- "typescript": "^5.3.3" + + +## v0.3.0 + +- Jan 2023 Engineering: + - types/vscode 1.74.0 + - typescript 4.9.4 + - vsce 2.15.0 + - parser 5.48.1 + - eslint-plugin 5.48.1 + +- June 2022 Engineering: + - esbuild 0.14.47 + - eslint-plugin 5.29.0 + - parser 5.29.0 + - eslint 8.18.0 + - node 18.0.0 + +- Engineering: + - Bumped vscode to 1.65.0. + - Updated development dependencies to latest versions. + - Updated CI pipeline to use PowerShell 5.1 on Windows 2019. + +- May 2022 Engineering + - eslint-plugin 5.22.0 + - parser 5.22.0 + - eslint 8.14.0 + - esbuild 0.14.38 + - vscode 1.66.0 + +## v0.2.0 + +- Added dependabot alerts +- Added build status in Docs + +## v0.1.0 + +- Initial preview release. + +https://marketplace.visualstudio.com/items?itemName=vicperdana.psdocs-vscode-preview diff --git a/packages/vscode-extension/CODE_OF_CONDUCT.md b/packages/vscode-extension/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..f9ba8cf6 --- /dev/null +++ b/packages/vscode-extension/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Microsoft Open Source Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). + +Resources: + +- [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/) +- [Microsoft Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +- Contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with questions or concerns diff --git a/packages/vscode-extension/CONTRIBUTING.md b/packages/vscode-extension/CONTRIBUTING.md new file mode 100644 index 00000000..e17d0392 --- /dev/null +++ b/packages/vscode-extension/CONTRIBUTING.md @@ -0,0 +1,103 @@ +# Contributing to PSDocs.Azure VSCode Extension + +Welcome, and thank you for your interest in contributing to PSDocs! + +There are many ways in which you can contribute, beyond writing code. +The goal of this document is to provide a high-level overview of how you can get involved. + +- [Reporting issues](#reporting-issues) +- Fix bugs or add features + +## Contributor License Agreement (CLA) + +This project welcomes contributions and suggestions. Most contributions require you to +agree to a Contributor License Agreement (CLA) declaring that you have the right to, +and actually do, grant us the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need +to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the +instructions provided by the bot. You will only need to do this once across all repositories using our CLA. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Reporting issues + +Have you identified a reproducible problem? +Have a feature request? +We want to hear about it! +Here's how you can make reporting your issue as effective as possible. + +### Look for an existing issue + +Before you create a new issue, please do a search in [open issues][issues] to see if the issue or feature request has already been filed. + +If you find your issue already exists, +make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). +Use a reaction in place of a "+1" comment: + +* 👍 - upvote + +## Contributing to code + +- Before writing a fix or feature enhancement, ensure that an issue is logged. +- Be prepared to discuss a feature and take feedback. +- Include unit tests and updates documentation to complement the change. + +When you are ready to contribute a fix or feature: + +- Start by [forking the PSDocs-vscode][github-fork]. +- Create a new branch from main in your fork. +- Add commits in your branch. + - If you have updated module code or rules also update `CHANGELOG.md`. + - You don't need to update the `CHANGELOG.md` for changes to unit tests or documentation. + - Try building your changes locally. See [building from source][build] for instructions. +- [Create a pull request][github-pr-create] to merge changes into the PSDocs `main` branch. + - If you are _ready_ for your changes to be reviewed create a _pull request_. + - If you are _not ready_ for your changes to be reviewed, create a _draft pull request_. + - An continuous integration (CI) process will automatically build your changes. + - You changes must build successfully to be merged. + - If you have any build errors, push new commits to your branch. + - Avoid using forced pushes or squashing changes while in review, as this makes reviewing your changes harder. + +### Intro to Git and GitHub + +When contributing to documentation or code changes, you'll need to have a GitHub account and a basic understanding of Git. +Check out the links below to get started. + +- Make sure you have a [GitHub account][github-signup]. +- GitHub Help: + - [Git and GitHub learning resources][learn-git]. + - [GitHub Flow Guide][github-flow]. + - [Fork a repo][github-fork]. + - [About Pull Requests][github-pr]. + +### Code editor + +You should use the multi-platform [Visual Studio Code][vscode] (VS Code). +The project contains a number of workspace specific settings that make it easier to author consistently. + +### Building and testing + +When creating a pull request to merge your changes, a continuous integration (CI) pipeline is run. +The CI pipeline will build then test your changes across MacOS, Linux and Windows configurations. + +Before opening a pull request try building your changes locally. + +## Thank You! + +Your contributions to open source, large or small, make great projects like this possible. +Thank you for taking the time to contribute. + +[learn-git]: https://help.github.com/en/articles/git-and-github-learning-resources +[github-flow]: https://guides.github.com/introduction/flow/ +[github-signup]: https://github.com/signup/free +[github-fork]: https://help.github.com/en/github/getting-started-with-github/fork-a-repo +[github-pr]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests +[github-pr-create]: https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request-from-a-fork +[vscode]: https://code.visualstudio.com/ +[issues]: https://github.com/Microsoft/PSDocs-vscode/issues diff --git a/packages/vscode-extension/LICENSE b/packages/vscode-extension/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/packages/vscode-extension/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/packages/vscode-extension/README.md b/packages/vscode-extension/README.md new file mode 100644 index 00000000..f8f963e3 --- /dev/null +++ b/packages/vscode-extension/README.md @@ -0,0 +1,97 @@ +# PSDocs.Azure + +[![Build Status](https://dev.azure.com/viperdan/PSDocs-vscode/_apis/build/status/PSDocs-vscode-ci?branchName=main)](https://dev.azure.com/viperdan/PSDocs-vscode/_build/latest?definitionId=50&branchName=main) +![VSCode Extension](https://img.shields.io/visual-studio-marketplace/d/vicperdana.PSDocs-vscode-preview?color=blue&label=VSCode%20Downloads) + +Generate documentation from Infrastructure as Code (IaC). PSDocs for Azure automatically generates documentation for Azure infrastructure as code (IaC) artifacts. + +Please review the [Requirements](#requirements) to ensure you can use this extension successfully. + +Note: this extension is in preview. + +## Features + +### Command Palette +You can generate markdown files directly from an ARM template through the Command Pallette. Simply press `Ctrl+Shift+P` (Win/Linux) or `Command+Shift+P` (MacOS) and type in `PSDocs.Azure: Generate Markdown` + +![Generate Markdown](images/cmd-a.png) + +You will first be asked to provide a full path to the ARM template. The prompt auto-populates with the full path of the currently opened file. + +![Provide full path to the ARM template file](images/cmd-b.png) + +Additionally, you will be asked to provide a relative path (from the ARM template) to store the generated markdown. + +![Provide destination relative path where markdown will be created](images/cmd-c.png) + +The markdown will be created in the folder relative to the the ARM template file. + +### Snippets + +Adds snippets for adding metadata tag within ARM templates. +* `psdocs-arm` can be used to add metadata at the template root schema +* `psdocs-arm-short` can be used to add metadata anywhere else e.g. parameters or variables + +![PSDocs.Azure Template](images/snippet-arm.gif) + +![PSDocs.Azure Template](images/snippet-arm-short.gif) + + +## Requirements + +PSDocs.Azure is required for this extension to work. + +To install the module use the following command from a PowerShell prompt. + +```powershell +Install-Module -Name PSDocs.Azure -Scope CurrentUser; +``` + +## Known Issues and Limitations + +* The extension is in preview and therefore has not undergone extended testing scenarios. +* Only one markdown can be generated at one time. +* A separate directory should be used to avoid overriding the Generated Markdown --> README.md file. +* Additional PSDocs.Azure [configuration](https://github.com/Azure/PSDocs.Azure/blob/main/docs/concepts/en-US/about_PSDocs_Azure_Configuration.md) is not supported at this time. + +## Release Notes + +Refer to [CHANGELOG](CHANGELOG.md) + +## Contributing + +This project welcomes contributions and suggestions. +If you are ready to contribute, please visit the [contribution guide]. + +## Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) +or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +## Maintainers + +- [Vic Perdana](https://github.com/VicPerdana) +- [Bernie White](https://github.com/BernieWhite) + +## License + +This project is [licensed under the MIT License][license]. + +[issue]: https://github.com/Microsoft/PSDocs-vscode/issues +[discussion]: https://github.com/microsoft/PSDocs-vscode/discussions +[ci-badge]: https://dev.azure.com/viperdan/PSDocs-vscode/_apis/build/status/PSDocs-vscode-CI?branchName=main +[vscode-ext-gallery]: https://code.visualstudio.com/docs/editor/extension-gallery +[ext-preview]: https://marketplace.visualstudio.com/items?itemName=viperdan.PSDocs-vscode-preview +[ext-preview-version-badge]: https://vsmarketplacebadge.apphb.com/version/viperdan.PSDocs-vscode-preview.svg +[ext-preview-installs-badge]: https://vsmarketplacebadge.apphb.com/installs-short/viperdan.PSDocs-vscode-preview.svg +[ext-stable]: https://marketplace.visualstudio.com/items?itemName=viperdan.PSDocs-vscode +[ext-stable-version-badge]: https://vsmarketplacebadge.apphb.com/version/viperdan.PSDocs-vscode.svg +[ext-stable-installs-badge]: https://vsmarketplacebadge.apphb.com/installs-short/viperdan.PSDocs-vscode.svg +[module-version-badge]: https://img.shields.io/powershellgallery/v/PSDocs.svg?label=PowerShell%20Gallery&color=brightgreen +[contribution guide]: https://github.com/Microsoft/PSDocs-vscode/blob/main/CONTRIBUTING.md +[change log]: https://github.com/Microsoft/PSDocs-vscode/blob/main/CHANGELOG.md +[license]: https://github.com/Microsoft/PSDocs-vscode/blob/main/LICENSE +[ps-rule.yaml]: https://microsoft.github.io/PSDocs/concepts/PSDocs/en-US/about_PSDocs_Options.html + + diff --git a/packages/vscode-extension/SECURITY.md b/packages/vscode-extension/SECURITY.md new file mode 100644 index 00000000..f7b89984 --- /dev/null +++ b/packages/vscode-extension/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). + +If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://docs.microsoft.com/en-us/previous-versions/tn-archive/cc751383(v=technet.10)), please report it to us as described below. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://msrc.microsoft.com/create-report). + +If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://www.microsoft.com/en-us/msrc/pgp-key-msrc). + +You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). + +Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://microsoft.com/msrc/bounty) page for more details about our active programs. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://www.microsoft.com/en-us/msrc/cvd). + + \ No newline at end of file diff --git a/packages/vscode-extension/SUPPORT.md b/packages/vscode-extension/SUPPORT.md new file mode 100644 index 00000000..97320fa7 --- /dev/null +++ b/packages/vscode-extension/SUPPORT.md @@ -0,0 +1,16 @@ +# Support + +## How to file issues and get help + +This project uses GitHub Issues to track bugs and feature requests. +Please search the existing issues before filing new issues to avoid duplicates. + +- For new issues, file your bug or feature request as a new [issue]. +- For help, discussion, and support questions about using this project, join or start a [discussion]. + +## Microsoft Support Policy + +Support for this project/ product is limited to the resources listed above. + +[issue]: https://github.com/microsoft/PSDocs-vscode/issues +[discussion]: https://github.com/microsoft/PSDocs-vscode/discussions diff --git a/packages/vscode-extension/images/cmd-a.png b/packages/vscode-extension/images/cmd-a.png new file mode 100644 index 00000000..3d403805 Binary files /dev/null and b/packages/vscode-extension/images/cmd-a.png differ diff --git a/packages/vscode-extension/images/cmd-b.png b/packages/vscode-extension/images/cmd-b.png new file mode 100644 index 00000000..be4aa3e2 Binary files /dev/null and b/packages/vscode-extension/images/cmd-b.png differ diff --git a/packages/vscode-extension/images/cmd-c.png b/packages/vscode-extension/images/cmd-c.png new file mode 100644 index 00000000..67206648 Binary files /dev/null and b/packages/vscode-extension/images/cmd-c.png differ diff --git a/packages/vscode-extension/images/psdocs-icon.png b/packages/vscode-extension/images/psdocs-icon.png new file mode 100644 index 00000000..a871cca5 Binary files /dev/null and b/packages/vscode-extension/images/psdocs-icon.png differ diff --git a/packages/vscode-extension/images/snippet-arm-a.png b/packages/vscode-extension/images/snippet-arm-a.png new file mode 100644 index 00000000..1fd3c874 Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm-a.png differ diff --git a/packages/vscode-extension/images/snippet-arm-b.png b/packages/vscode-extension/images/snippet-arm-b.png new file mode 100644 index 00000000..0bcbde96 Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm-b.png differ diff --git a/packages/vscode-extension/images/snippet-arm-short-a.png b/packages/vscode-extension/images/snippet-arm-short-a.png new file mode 100644 index 00000000..9ef07414 Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm-short-a.png differ diff --git a/packages/vscode-extension/images/snippet-arm-short-b.png b/packages/vscode-extension/images/snippet-arm-short-b.png new file mode 100644 index 00000000..3b38d02d Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm-short-b.png differ diff --git a/packages/vscode-extension/images/snippet-arm-short.gif b/packages/vscode-extension/images/snippet-arm-short.gif new file mode 100644 index 00000000..4ce46797 Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm-short.gif differ diff --git a/packages/vscode-extension/images/snippet-arm.gif b/packages/vscode-extension/images/snippet-arm.gif new file mode 100644 index 00000000..b62258ba Binary files /dev/null and b/packages/vscode-extension/images/snippet-arm.gif differ diff --git a/packages/vscode-extension/package-lock.json b/packages/vscode-extension/package-lock.json new file mode 100644 index 00000000..91d2dd57 --- /dev/null +++ b/packages/vscode-extension/package-lock.json @@ -0,0 +1,5594 @@ +{ + "name": "psdocs-vscode", + "version": "0.3.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "psdocs-vscode", + "version": "0.3.3", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@vscode/vsce": "^3.3.2" + }, + "devDependencies": { + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.6", + "@types/node": "^24.6.1", + "@types/vscode": "^1.99.1", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "esbuild": "^0.25.10", + "eslint": "^9.39.1", + "glob": "^11.1.0", + "mocha": "^11.1.0", + "typescript": "^5.4.5", + "vscode-test": "^1.6.1" + }, + "engines": { + "vscode": "^1.89.0" + } + }, + "node_modules/@azure/abort-controller": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-1.1.0.tgz", + "integrity": "sha512-TrRLIoSQVzfAJX9H1JeFjzAoDGcoK1IYX1UImfceTZpsyYfWr09Ss1aHW1y5TrrR3iq6RZLBwJ3E24uwPhwahw==", + "dependencies": { + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz", + "integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz", + "integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.9.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.1.2.tgz", + "integrity": "sha512-dawW9ifvWAWmUm9/h+/UQ2jrdvjCJ7VJEuCJ6XVNudzcOwm53BFZH4Q845vjfgoUAM8ZxokvVNxNxAITc502YA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz", + "integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util/node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.2.1.tgz", + "integrity": "sha512-U8hsyC9YPcEIzoaObJlRDvp7KiF0MGS7xcWbyJSVvXRkC/HXo1f0oYeBYmEvVgRfacw7GHf6D6yAoh9JHz6A5Q==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.5.0", + "@azure/core-client": "^1.4.0", + "@azure/core-rest-pipeline": "^1.1.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.3.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^3.11.1", + "@azure/msal-node": "^2.9.2", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^8.0.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.2.tgz", + "integrity": "sha512-l170uE7bsKpIU6B/giRc9i4NI0Mj+tANMMMxf7Zi/5cKzEqPayP7+X1WPrG7e+91JgY8N+7K7nF2WOi7iVhXvg==", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.14.0.tgz", + "integrity": "sha512-Un85LhOoecJ3HDTS3Uv3UWnXC9/43ZSO+Kc+anSqpZvcEt58SiO/3DuVCAe1A3I5UIBYJNMgTmZPGXQ0MVYrwA==", + "dependencies": { + "@azure/msal-common": "14.10.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "14.10.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.10.0.tgz", + "integrity": "sha512-Zk6DPDz7e1wPgLoLgAp0349Yay9RvcjPM5We/ehuenDNsz/t9QEFI7tRoHpp/e47I4p20XE3FiDlhKwAo3utDA==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.9.2.tgz", + "integrity": "sha512-8tvi6Cos3m+0KmRbPjgkySXi+UQU/QiuVRFnrxIwt5xZlEEFa69O04RTaNESGgImyBBlYbo2mfE8/U8Bbdk1WQ==", + "dependencies": { + "@azure/msal-common": "14.12.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "14.12.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.12.0.tgz", + "integrity": "sha512-IDDXmzfdwmDkv4SSmMEyAniJf6fDu3FJ7ncOjlxkDuT85uSnLEhZi3fGZpoR7T4XZpOMx9teM9GXBgrfJgyeBw==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/estree": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.6.1.tgz", + "integrity": "sha512-ljvjjs3DNXummeIaooB4cLBKg2U6SPI6Hjra/9rRIy7CpM0HpLtG9HptkMKAb4HYWy5S7HUvJEuWgr/y0U8SHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.13.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.99.1", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.99.1.tgz", + "integrity": "sha512-cQlqxHZ040ta6ovZXnXRxs3fJiTmlurkIWOfZVcLSZPcm9J4ikFpXuB7gihofGn5ng+kDVma5EmJIclfk0trPQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.45.0.tgz", + "integrity": "sha512-HC3y9CVuevvWCl/oyZuI47dOeDF9ztdMEfMH8/DW/Mhwa9cCLnK1oD7JoTVGW/u7kFzNZUKUoyJEqkaJh5y3Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/type-utils": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.45.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.45.0.tgz", + "integrity": "sha512-TGf22kon8KW+DeKaUmOibKWktRY8b2NSAZNdtWh798COm1NWx8+xJ6iFBtk3IvLdv6+LGLJLRlyhrhEDZWargQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.45.0.tgz", + "integrity": "sha512-3pcVHwMG/iA8afdGLMuTibGR7pDsn9RjDev6CCB+naRsSYs2pns5QbinF4Xqw6YC/Sj3lMrm/Im0eMfaa61WUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.45.0", + "@typescript-eslint/types": "^8.45.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.45.0.tgz", + "integrity": "sha512-clmm8XSNj/1dGvJeO6VGH7EUSeA0FMs+5au/u3lrA3KfG8iJ4u8ym9/j2tTEoacAffdW1TVUzXO30W1JTJS7dA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.45.0.tgz", + "integrity": "sha512-aFdr+c37sc+jqNMGhH+ajxPXwjv9UtFZk79k8pLoJ6p4y0snmYpPA52GuWHgt2ZF4gRRW6odsEj41uZLojDt5w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.45.0.tgz", + "integrity": "sha512-bpjepLlHceKgyMEPglAeULX1vixJDgaKocp0RVJ5u4wLJIMNuKtUXIczpJCPcn2waII0yuvks/5m5/h3ZQKs0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0", + "@typescript-eslint/utils": "8.45.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.45.0.tgz", + "integrity": "sha512-WugXLuOIq67BMgQInIxxnsSyRLFxdkJEJu8r4ngLR56q/4Q5LrbfkFRH27vMTjxEK8Pyz7QfzuZe/G15qQnVRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.45.0.tgz", + "integrity": "sha512-GfE1NfVbLam6XQ0LcERKwdTTPlLvHvXXhOeUGC1OXi4eQBoyy1iVsW+uzJ/J9jtCz6/7GCQ9MtrQ0fml/jWCnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.45.0", + "@typescript-eslint/tsconfig-utils": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/visitor-keys": "8.45.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.45.0.tgz", + "integrity": "sha512-bxi1ht+tLYg4+XV2knz/F7RVhU0k6VrSMc9sb8DQ6fyCTrGQLHfo7lDtN0QJjZjKkLA2ThrKuCdHEvLReqtIGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.45.0", + "@typescript-eslint/types": "8.45.0", + "@typescript-eslint/typescript-estree": "8.45.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.45.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.45.0.tgz", + "integrity": "sha512-qsaFBA3e09MIDAGFUrTk+dzqtfv1XPVz8t8d1f0ybTzrCY7BKiMC5cjrl1O/P7UmHsNyW90EYSkU/ZWpmXelag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.45.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vscode/vsce": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-3.3.2.tgz", + "integrity": "sha512-XQ4IhctYalSTMwLnMS8+nUaGbU7v99Qm2sOoGfIEf2QC7jpiLXZZMh7NwArEFsKX4gHTJLx0/GqAUlCdC3gKCw==", + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^12.1.0", + "form-data": "^4.0.0", + "glob": "^11.0.0", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^14.1.0", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.3", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.4.tgz", + "integrity": "sha512-0uL32egStKYfy60IqnynAChMTbL0oqpqk0Ew0YHiIb+fayuGZWADuIPHWUcY1GCnAA+VgchOPDMxnc2R3XGWEA==", + "hasInstallScript": true, + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.2", + "@vscode/vsce-sign-alpine-x64": "2.0.2", + "@vscode/vsce-sign-darwin-arm64": "2.0.2", + "@vscode/vsce-sign-darwin-x64": "2.0.2", + "@vscode/vsce-sign-linux-arm": "2.0.2", + "@vscode/vsce-sign-linux-arm64": "2.0.2", + "@vscode/vsce-sign-linux-x64": "2.0.2", + "@vscode/vsce-sign-win32-arm64": "2.0.2", + "@vscode/vsce-sign-win32-x64": "2.0.2" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.2.tgz", + "integrity": "sha512-E80YvqhtZCLUv3YAf9+tIbbqoinWLCO/B3j03yQPbjT3ZIHCliKZlsy1peNc4XNZ5uIb87Jn0HWx/ZbPXviuAQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.2.tgz", + "integrity": "sha512-n1WC15MSMvTaeJ5KjWCzo0nzjydwxLyoHiMJHu1Ov0VWTZiddasmOQHekA47tFRycnt4FsQrlkSCTdgHppn6bw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.2.tgz", + "integrity": "sha512-rz8F4pMcxPj8fjKAJIfkUT8ycG9CjIp888VY/6pq6cuI2qEzQ0+b5p3xb74CJnBbSC0p2eRVoe+WgNCAxCLtzQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.2.tgz", + "integrity": "sha512-MCjPrQ5MY/QVoZ6n0D92jcRb7eYvxAujG/AH2yM6lI0BspvJQxp0o9s5oiAM9r32r9tkLpiy5s2icsbwefAQIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.2.tgz", + "integrity": "sha512-Fkb5jpbfhZKVw3xwR6t7WYfwKZktVGNXdg1m08uEx1anO0oUPUkoQRsNm4QniL3hmfw0ijg00YA6TrxCRkPVOQ==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.2.tgz", + "integrity": "sha512-Ybeu7cA6+/koxszsORXX0OJk9N0GgfHq70Wqi4vv2iJCZvBrOWwcIrxKjvFtwyDgdeQzgPheH5nhLVl5eQy7WA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.2.tgz", + "integrity": "sha512-NsPPFVtLaTlVJKOiTnO8Cl78LZNWy0Q8iAg+LlBiCDEgC12Gt4WXOSs2pmcIjDYzj2kY4NwdeN1mBTaujYZaPg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.2.tgz", + "integrity": "sha512-wPs848ymZ3Ny+Y1Qlyi7mcT6VSigG89FWQnp2qRYCyMhdJxOpA4lDwxzlpL8fG6xC8GjQjGDkwbkWUcCobvksQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.2.tgz", + "integrity": "sha512-pAiRN6qSAhDM5SVOIxgx+2xnoVUePHbRNC7OD2aOR3WltTKxxF25OfpK8h8UQ7A0BuRkSgREbB59DBlFk4iAeg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@vscode/vsce/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "optional": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "dev": true + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, + "node_modules/buffer-indexof-polyfill": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz", + "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "optional": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/cliui/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/cockatiel": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.1.3.tgz", + "integrity": "sha512-xC759TpZ69d7HhfDp8m2WkRwEUiCkxY8Ee2OQH/3H6zmy2D/5Sm+zSTbPRa+V2QyjDtpMvjOIAOVjA2gp6N1kQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "optional": true, + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "optional": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "9.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "optional": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/fstream": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", + "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "inherits": "~2.0.0", + "mkdirp": ">=0.5 0", + "rimraf": "2" + }, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/fstream/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/fstream/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fstream/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/fstream/node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "optional": true + }, + "node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/brace-expansion": "^5.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", + "integrity": "sha512-wlwpilI7YdjSkWaQ/7omYBMTliDcmCN8OLihO6I9B86g06lMyAoqgoDpV0XqoaPOKj+0DIdAvnsWfyAAhmimcg==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/ignore": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "devOptional": true + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "optional": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/jackspeak": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz", + "integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonc-parser": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/jwa": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", + "integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jsonwebtoken/node_modules/jws": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.2", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/listenercount": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz", + "integrity": "sha512-3mk/Zag0+IJxeDrxSgaDPy4zZ3w05PRZeJNnlWhzFz5OkX49J4krc+A8X2d2M69vGMBEX0uyl8M+W+8gH+kBqQ==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "optional": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "devOptional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "optional": true + }, + "node_modules/mocha": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", + "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "browser-stdout": "^1.3.1", + "chokidar": "^3.5.3", + "debug": "^4.3.5", + "diff": "^5.2.0", + "escape-string-regexp": "^4.0.0", + "find-up": "^5.0.0", + "glob": "^10.4.5", + "he": "^1.2.0", + "js-yaml": "^4.1.0", + "log-symbols": "^4.1.0", + "minimatch": "^5.1.6", + "ms": "^2.1.3", + "serialize-javascript": "^6.0.2", + "strip-json-comments": "^3.1.1", + "supports-color": "^8.1.1", + "workerpool": "^6.5.1", + "yargs": "^17.7.2", + "yargs-parser": "^21.1.1", + "yargs-unparser": "^2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/mocha/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mocha/node_modules/glob": { + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/mocha/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/mocha/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/mocha/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mocha/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==", + "optional": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-abi": { + "version": "3.62.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.62.0.tgz", + "integrity": "sha512-CPMcGa+y33xuL1E0TcNIu4YyaZCxnnvkVaEXrsosR3FxN+fV8xvb7Mzpb7IgKler10qeMkE6+Dp8qJhpzdq35g==", + "optional": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "devOptional": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", + "integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "optional": true, + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, + "node_modules/semver": { + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "optional": true, + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "optional": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "optional": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tmp": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.4.tgz", + "integrity": "sha512-UdiSoX6ypifLmrfQ/XfiawN6hkjSBpCjhKxxZcWlUUmoXLaCKQU0bx4HF/tdDK2uzRuchf1txGvrWBzYREssoQ==", + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "optional": true, + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "dev": true, + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici-types": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.13.0.tgz", + "integrity": "sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unzipper": { + "version": "0.10.14", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.14.tgz", + "integrity": "sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==", + "dev": true, + "dependencies": { + "big-integer": "^1.6.17", + "binary": "~0.3.0", + "bluebird": "~3.4.1", + "buffer-indexof-polyfill": "~1.0.0", + "duplexer2": "~0.1.4", + "fstream": "^1.0.12", + "graceful-fs": "^4.2.2", + "listenercount": "~1.0.1", + "readable-stream": "~2.3.6", + "setimmediate": "~1.0.4" + } + }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "devOptional": true + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vscode-test": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/vscode-test/-/vscode-test-1.6.1.tgz", + "integrity": "sha512-086q88T2ca1k95mUzffvbzb7esqQNvJgiwY4h29ukPhFo8u+vXOOmelUoU5EQUHs3Of8+JuQ3oGdbVCqaxuTXA==", + "deprecated": "This package has been renamed to @vscode/test-electron, please update to the new name", + "dev": true, + "dependencies": { + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "rimraf": "^3.0.2", + "unzipper": "^0.10.11" + }, + "engines": { + "node": ">=8.9.3" + } + }, + "node_modules/vscode-test/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/vscode-test/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "dev": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/vscode-test/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/workerpool": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", + "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "devOptional": true + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json new file mode 100644 index 00000000..29b19e61 --- /dev/null +++ b/packages/vscode-extension/package.json @@ -0,0 +1,93 @@ +{ + "name": "psdocs-vscode", + "displayName": "PSDocs.Azure (Preview)", + "publisher": "vicperdana", + "description": "Generate Markdown from ARM templates using PSDocs.Azure", + "author": { + "name": "Vic Perdana" + }, + "version": "0.3.3", + "engines": { + "vscode": "^1.89.0" + }, + "license": "SEE LICENSE IN LICENSE", + "categories": [ + "Programming Languages", + "Snippets" + ], + "keywords": [ + "Azure Template", + "Azure", + "ARM", + "Resource Manager", + "PSDocs.Azure" + ], + "galleryBanner": { + "color": "#0072c6", + "theme": "dark" + }, + "icon": "images/psdocs-icon.png", + "repository": { + "type": "git", + "url": "https://github.com/Azure/PSDocs.Azure.git", + "directory": "packages/vscode-extension" + }, + "bugs": { + "url": "https://github.com/Azure/PSDocs.Azure/issues" + }, + "activationEvents": [ + "onCommand:vicperdana.psdocs-vscode-preview" + ], + "private": true, + "preview": true, + "main": "./out/extension.js", + "contributes": { + "commands": [ + { + "command": "vicperdana.psdocs-vscode-preview", + "title": "PSDocs.Azure: Generate Markdown" + } + ], + "snippets": [ + { + "language": "arm-template", + "path": "./snippets/arm.json" + } + ] + }, + "scripts": { + "vscode:prepublish": "npm run -S esbuild-base -- --minify", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "pretest": "npm run compile", + "lint": "eslint src --ext ts", + "test": "node ./out/test/runTest.js", + "pack": "vsce package", + "publish": "vsce publish", + "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node", + "esbuild": "npm run -S esbuild-base -- --sourcemap", + "esbuild-watch": "npm run -S esbuild-base -- --sourcemap --watch" + }, + "extensionDependencies": [ + "vscode.powershell", + "ms-vscode.powershell", + "msazurermtools.azurerm-vscode-tools" + ], + "devDependencies": { + "@types/glob": "^8.1.0", + "@types/mocha": "^10.0.6", + "@types/node": "^24.6.1", + "@types/vscode": "^1.99.1", + "@typescript-eslint/eslint-plugin": "^8.45.0", + "@typescript-eslint/parser": "^8.45.0", + "esbuild": "^0.25.10", + "eslint": "^9.39.1", + "glob": "^11.1.0", + "mocha": "^11.1.0", + "typescript": "^5.4.5", + "vscode-test": "^1.6.1" + }, + "dependencies": { + "@vscode/vsce": "^3.3.2" + } +} diff --git a/packages/vscode-extension/pipeline.build.ps1 b/packages/vscode-extension/pipeline.build.ps1 new file mode 100644 index 00000000..ee7713df --- /dev/null +++ b/packages/vscode-extension/pipeline.build.ps1 @@ -0,0 +1,205 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Invoke-Build +# CI pipeline script for PSDocs-vscode + +[CmdletBinding()] +param ( + [Parameter(Mandatory = $False)] + [String]$Build = '0.0.1', + + [Parameter(Mandatory = $False)] + [ValidateSet('preview', 'stable', 'dev')] + [String]$Channel, + + [Parameter(Mandatory = $False)] + [String]$Configuration = 'Debug', + + [Parameter(Mandatory = $False)] + [String]$OutputPath = (Join-Path -Path $PWD -ChildPath out), + + [Parameter(Mandatory = $False)] + [String]$ApiKey, + + [Parameter(Mandatory = $False)] + [String]$AssertStyle = 'AzurePipelines' +) + +$commitId = git log --format="%H" -n 1; + +Write-Host -Object "[Pipeline] -- PWD: $PWD" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- OutputPath: $OutputPath" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- BuildNumber: $($Env:BUILD_BUILDNUMBER)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- CommitId: $($commitId)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- SourceBranch: $($Env:BUILD_SOURCEBRANCH)" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- SourceBranchName: $($Env:BUILD_SOURCEBRANCHNAME)" -ForegroundColor Green; + +if ($Env:SYSTEM_DEBUG -eq 'true') { + $VerbosePreference = 'Continue'; +} + +if ($Env:BUILD_SOURCEBRANCH -like '*/tags/*' -and $Env:BUILD_SOURCEBRANCHNAME -like 'v1.*') { + $Build = $Env:BUILD_SOURCEBRANCHNAME.Substring(1); +} + +$version = $Build; + +# Handle channel +if ([String]::IsNullOrEmpty($Channel)) { + $Channel = 'preview'; +} +$channelSuffix = '-preview'; +$channelDisplayName = 'PSDocs.Azure (Preview)'; +switch ($Channel) { + 'dev' { $channelSuffix = '-dev'; $channelDisplayName = 'PSDocs.Azure (Dev)'; } + 'stable' { $channelSuffix = ''; $channelDisplayName = 'PSDocs.Azure'; } + default { $channelSuffix = '-preview'; $channelDisplayName = 'PSDocs.Azure (Preview)'; } +} + +Write-Host -Object "[Pipeline] -- Using channel: $Channel" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- Using channelSuffix: $channelSuffix" -ForegroundColor Green; +Write-Host -Object "[Pipeline] -- Using version: $version" -ForegroundColor Green; + +$packageRoot = Join-Path -Path $OutputPath -ChildPath 'package'; +$packageName = "PSDocs-vscode$channelSuffix"; +$packagePath = Join-Path -Path $packageRoot -ChildPath "$packageName.vsix"; + +function Get-RepoRuleData { + [CmdletBinding()] + param ( + [Parameter(Position = 0, Mandatory = $False)] + [String]$Path = $PWD + ) + process { + GetPathInfo -Path $Path -Verbose:$VerbosePreference; + } +} + +function GetPathInfo { + [CmdletBinding()] + param ( + [Parameter(Mandatory = $True)] + [String]$Path + ) + begin { + $items = New-Object -TypeName System.Collections.ArrayList; + } + process { + $Null = $items.Add((Get-Item -Path $Path)); + $files = @(Get-ChildItem -Path $Path -File -Recurse -Include *.ps1,*.psm1,*.psd1,*.cs | Where-Object { + !($_.FullName -like "*.Designer.cs") -and + !($_.FullName -like "*/bin/*") -and + !($_.FullName -like "*/obj/*") -and + !($_.FullName -like "*\obj\*") -and + !($_.FullName -like "*\bin\*") -and + !($_.FullName -like "*\out\*") -and + !($_.FullName -like "*/out/*") + }); + $Null = $items.AddRange($files); + } + end { + $items; + } +} + +task BuildExtension { + Write-Host '> Building extension' -ForegroundColor Green; + exec { & npm run compile } +} + +task PackageExtension { + Write-Host '> Packaging PSDocs-vscode' -ForegroundColor Green; + if (!(Test-Path -Path $packageRoot)) { + $Null = New-Item -Path $packageRoot -ItemType Directory -Force; + } + exec { & npm run pack -- --out $packagePath } +} + +# Synopsis: Install the extension in Visual Studio Code +task InstallExtension { + Write-Host '> Installing PSDocs-vscode' -ForegroundColor Green; + exec { & code --install-extension $packagePath --force } +} + +task VersionExtension { + # Update channel name + $package = Get-Content ./package.json -Raw | ConvertFrom-Json; + if ($package.name -ne $packageName) { + $package.name = $packageName; + $package | ConvertTo-Json -Depth 99 | Set-Content ./package.json; + } + + # Update channel flag + $package = Get-Content ./package.json -Raw | ConvertFrom-Json; + $previewFlag = $Channel -ne 'stable'; + if ($package.preview -ne $previewFlag) { + $package.preview = $previewFlag; + $package | ConvertTo-Json -Depth 99 | Set-Content ./package.json; + } + + # Update channel display name + $package = Get-Content ./package.json -Raw | ConvertFrom-Json; + if ($package.displayName -ne $channelDisplayName) { + $package.displayName = $channelDisplayName; + $package | ConvertTo-Json -Depth 99 | Set-Content ./package.json; + } + + if (![String]::IsNullOrEmpty($Build)) { + # Update extension version + if (![String]::IsNullOrEmpty($version)) { + Write-Verbose -Message "[VersionExtension] -- Updating extension version"; + $package = Get-Content ./package.json -Raw | ConvertFrom-Json; + + if ($package.version -ne $version) { + $package.version = $version; + $package | ConvertTo-Json -Depth 99 | Set-Content ./package.json; + } + } + } +} + +# Synopsis: Install NuGet provider +task NuGet { + if ($Null -eq (Get-PackageProvider -Name NuGet -ErrorAction Ignore)) { + Install-PackageProvider -Name NuGet -Force -Scope CurrentUser; + } +} + +# Synopsis: Install PSDocs +task PSDocs NuGet, { + if ($Null -eq (Get-InstalledModule -Name PSDocs.Azure -MinimumVersion 0.3.0 -ErrorAction Ignore)) { + Install-Module -Name PSDocs.Azure -Repository PSGallery -MinimumVersion 0.3.0 -Scope CurrentUser -Force; + } + Import-Module -Name PSDocs -Verbose:$False; +} + +# Synopsis: Remove temp files +task Clean { + Remove-Item -Path out,reports -Recurse -Force -ErrorAction Ignore; +} + +# Synopsis: Restore NPM packages +task PackageRestore { + exec { & npm install --no-save } +} + +task ReleaseExtension { + exec { & npm install vsce --no-save } + exec { & npm run publish -- --packagePath $packagePath --pat $ApiKey } +} + +# Synopsis: Add shipit build tag +task TagBuild { + if ($Null -ne $Env:BUILD_DEFINITIONNAME) { + Write-Host "`#`#vso[build.addbuildtag]shipit"; + } +} + +task Build Clean, PackageRestore, VersionExtension, PackageExtension + +task Install Build, InstallExtension + +task . Build + +task Release VersionExtension, ReleaseExtension, TagBuild diff --git a/packages/vscode-extension/snippets/arm.json b/packages/vscode-extension/snippets/arm.json new file mode 100644 index 00000000..0f3a3391 --- /dev/null +++ b/packages/vscode-extension/snippets/arm.json @@ -0,0 +1,21 @@ +{ + "psdocs-arm": { + "prefix": "psdocs-arm", + "description": "Add metadata to an existing ARM template", + "body": [ + "\"metadata\": {", + "\t\"name\": \"${1:Name of the template}\",", + "\t\"description\": \"${2:Description of the template}\"", + "}" + ] + }, + "psdocs-arm-parameter": { + "prefix": "psdocs-arm-short", + "description": "Add metadata to an existing parameter or variable in an ARM template", + "body": [ + "\"metadata\": {", + "\t\"description\": \"${1:Description of the parameter or variable}\"", + "}" + ] + } +} diff --git a/packages/vscode-extension/src/extension.ts b/packages/vscode-extension/src/extension.ts new file mode 100644 index 00000000..f442dae0 --- /dev/null +++ b/packages/vscode-extension/src/extension.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +import path = require("path"); +import * as vscode from "vscode"; + +export function activate(context: vscode.ExtensionContext) { + let disposable = vscode.commands.registerCommand( + "vicperdana.psdocs-vscode-preview", + function () { + var currentlyOpenFilePath = + vscode.window.activeTextEditor?.document.uri.fsPath; + + var templatePath = ""; + var outputPath = ""; + var templateFolderPath = ""; + let options: vscode.InputBoxOptions = { + prompt: "Full path to the ARM template: ", + value: `${currentlyOpenFilePath}`, + }; + + vscode.window.showInputBox(options).then((value) => { + if (!value) return; + templatePath = value; + templateFolderPath = path.dirname(templatePath); + let options: vscode.InputBoxOptions = { + prompt: + "Output Path of the Markdown (relative path from the ARM template file): ", + value: "out\\docs\\", + }; + vscode.window.showInputBox(options).then((value) => { + if (!value) return; + outputPath = value; + + const { exec } = require("child_process"); + var message = ""; + exec( + `Import-Module PSDocs.Azure; Invoke-PSDocument -Module PSDocs.Azure -InputObject ${templatePath} -OutputPath ${templateFolderPath}/${outputPath};`, + { shell: "pwsh" }, + (error: any, stdout: any, stderr: any) => { + message = stderr; + if (message !== "") { + vscode.window.showErrorMessage(message); + } else { + vscode.window.showInformationMessage( + `Markdown generated in ${templateFolderPath}/${outputPath}` + ); + } + } + ); + }); + }); + } + ); +} + +export function deactivate() {} diff --git a/packages/vscode-extension/src/test/runTest.ts b/packages/vscode-extension/src/test/runTest.ts new file mode 100644 index 00000000..1eabfa33 --- /dev/null +++ b/packages/vscode-extension/src/test/runTest.ts @@ -0,0 +1,23 @@ +import * as path from 'path'; + +import { runTests } from 'vscode-test'; + +async function main() { + try { + // The folder containing the Extension Manifest package.json + // Passed to `--extensionDevelopmentPath` + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // The path to test runner + // Passed to --extensionTestsPath + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // Download VS Code, unzip it and run the integration test + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('Failed to run tests'); + process.exit(1); + } +} + +main(); diff --git a/packages/vscode-extension/src/test/suite/extension.test.ts b/packages/vscode-extension/src/test/suite/extension.test.ts new file mode 100644 index 00000000..6249ea48 --- /dev/null +++ b/packages/vscode-extension/src/test/suite/extension.test.ts @@ -0,0 +1,8 @@ +import * as assert from "assert"; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from "vscode"; +// import * as myExtension from '../../extension'; + +// to be added diff --git a/packages/vscode-extension/src/test/suite/index.ts b/packages/vscode-extension/src/test/suite/index.ts new file mode 100644 index 00000000..ede2342d --- /dev/null +++ b/packages/vscode-extension/src/test/suite/index.ts @@ -0,0 +1,39 @@ +import * as glob from 'glob'; +import * as Mocha from 'mocha'; +import * as path from 'path'; + +export function run(): Promise { + // Create the mocha test + const mocha = new Mocha({ + ui: 'tdd', + color: true, + }); + + const testsRoot = path.resolve(__dirname, '..'); + const { default: glob } = require('glob'); + + return new Promise((c, e) => { + glob('**/**.test.js', { cwd: testsRoot }, (err: any, files: string[]) => { + if (err) { + return e(err); + } + + // Add files to the test suite + files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // Run the mocha test + mocha.run((failures) => { + if (failures > 0) { + e(new Error(`${failures} tests failed.`)); + } else { + c(); + } + }); + } catch (err) { + console.error(err); + e(err); + } + }); + }); +} diff --git a/packages/vscode-extension/tsconfig.json b/packages/vscode-extension/tsconfig.json new file mode 100644 index 00000000..b9534e26 --- /dev/null +++ b/packages/vscode-extension/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es6", + "outDir": "out", + "lib": ["es6"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */ + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + }, + "exclude": ["node_modules", ".vscode-test"] +}