Documentarian.MicrosoftDocs.psm1
|
# Copyright (c) Microsoft Corporation. # Licensed under the MIT License. #region Classes.Public class LearnLocales { ## This list of locales is based on the language selection page on the Learn platform. static [string[]]$SupportedLocales = 'ar-sa', 'bg-bg', 'bs-latn-ba', 'ca-es', 'cs-cz', 'da-dk', 'de-at', 'de-ch', 'de-de', 'el-gr', 'en-au', 'en-ca', 'en-gb', 'en-ie', 'en-in', 'en-my', 'en-nz', 'en-sg', 'en-us', 'en-za', 'es-es', 'es-mx', 'et-ee', 'eu-es', 'fi-fi', 'fil-ph', 'fr-be', 'fr-ca', 'fr-ch', 'fr-fr', 'ga-ie', 'gl-es', 'he-il', 'hi-in', 'hr-hr', 'hu-hu', 'id-id', 'is-is', 'it-ch', 'it-it', 'ja-jp', 'ka-ge', 'kk-kz', 'ko-kr', 'lb-lu', 'lt-lt', 'lv-lv', 'ms-my', 'mt-mt', 'nb-no', 'nl-be', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ro-ro', 'ru-ru', 'sk-sk', 'sl-si', 'sr-cyrl-rs', 'sr-latn-rs', 'sv-se', 'th-th', 'tr-tr', 'uk-ua', 'vi-vn', 'zh-cn', 'zh-hk', 'zh-tw' ## Most docs are trnaslated into a subset of 19 locales. static [string[]]$CommonLocales = 'en-us', 'cs-cz', 'de-de', 'es-es', 'fr-fr', 'hu-hu', 'id-id', 'it-it', 'ja-jp', 'ko-kr', 'nl-nl', 'pl-pl', 'pt-br', 'pt-pt', 'ru-ru', 'sv-se', 'tr-tr', 'zh-cn', 'zh-tw' } #endregion Classes.Public #region Functions.Public function Export-MonikerContent { [CmdletBinding()] [OutputType([System.IO.FileInfo])] param ( [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)] [SupportsWildcards()] [Alias("PSPath")] [string[]]$Path, [Parameter(Mandatory, Position = 1)] [string]$Moniker, [Parameter(Position = 2)] [string]$OutputPath = '.' ) begin { if (Test-Path -Path $OutputPath) { $OutputPath = (Resolve-Path -Path $OutputPath).Path } else { $OutputPath = New-Item -ItemType Directory -Path $OutputPath -Force } $monikerStartPattern = '::: moniker range="(?<comparison>[=\>\<]{1,2})\s*(?<name>.+)"' $monikerEndPattern = '::: moniker-end' function parseMoniker { param ( [string]$mdString ) if ($mdString -match $monikerStartPattern) { @{ Name = $matches['name'].Trim() Comparison = $matches['comparison'] } } } } process { foreach ($path in $Path) { $getChildItemSplat = @{ Path = $path File = $true ErrorAction = 'SilentlyContinue' } $resolvedFiles = Get-ChildItem @getChildItemSplat foreach ($file in $resolvedFiles) { if ($file.Extension -ne '.md') { Write-Verbose "Skipping non-markdown file: '$($file.FullName)'" continue } $outFilePath = Join-Path -Path $OutputPath "$($file.BaseName)_$Moniker.md" $mdContent = Get-Content -Path $file.FullName $currentMoniker = @{ Name = '' Comparison = '=' } $filteredContent = @() foreach ($line in $mdContent) { if ($line -match $monikerStartPattern) { $currentMoniker = parseMoniker $line } if ($currentMoniker.Name -eq '') { $filteredContent += $line } else { # check if the current moniker range includes the specified moniker switch ($currentMoniker.Comparison) { '=' { if ($Moniker -eq $currentMoniker.Name) { $filteredContent += $line } } '>=' { if ($Moniker -ge $currentMoniker.Name) { $filteredContent += $line } } '>' { if ($Moniker -gt $currentMoniker.Name) { $filteredContent += $line } } '<=' { if ($Moniker -le $currentMoniker.Name) { $filteredContent += $line } } '<' { if ($Moniker -lt $currentMoniker.Name) { $filteredContent += $line } } } } # end else for moniker range check if ($line -match $monikerEndPattern) { $currentMoniker = @{ Name = '' Comparison = '=' } } } #end foreach line Write-Verbose "Exported filtered content to '$outFilePath'" $filteredContent | Out-File -FilePath $outFilePath -Encoding utf8 Get-Item -Path $outFilePath } #end foreach file } #end foreach path } # end process block } function Get-CmdletXref { <# .SYNOPSIS Gets a cross-reference link for a command. #> [CmdletBinding()] [OutputType('System.String[]')] param( # The name of the command to get a cross-reference link for. [Parameter(Mandatory, ValueFromPipeline)] [string[]]$Name ) begin { $ProgressPreference = 'SilentlyContinue' } process { foreach ($cmdname in $Name) { try { $cmd = Get-Command $cmdname -ErrorAction Stop if ($cmd.CommandType -eq 'Alias') { Write-Verbose "$cmdname is an alias for $($cmd.ResolvedCommand)" $cmd = Get-Command $cmd.ResolvedCommand -ErrorAction Stop } $modulename = $cmd.ModuleName $commandname = $cmd.Name $commandtype = $cmd.CommandType if (@('Cmdlet', 'Function') -notcontains $commandtype) { Write-Verbose "$commandname is a(n) $commandtype" continue } elseif ($modulename -eq '') { $help = Get-Help $cmdname if ($help.modulename -ne '') { $modulename = $help.ModuleName $commandname = $help.Name $commandtype = $help.Category } } if ($modulename -eq '') { Write-Verbose "$commandname is an anonymous $commandtype." } else { "[$commandname](xref:${modulename}.$commandname)" } } catch [System.Management.Automation.CommandNotFoundException] { Write-Warning "Unable to find command $cmdname" } } } } function Get-HtmlMetaTags { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [uri]$ArticleUrl, [switch]$ShowRequiredMetadata ) $hash = [ordered]@{} $x = Invoke-WebRequest $ArticleUrl $lines = (($x -split "`n").trim() | Select-String -Pattern '\<meta').line | ForEach-Object { $_.trimstart('<meta ').trimend(' />') | Sort-Object } $pattern = '(name|property)="(?<key>[^"]+)"\s*content="(?<value>[^"]+)"' foreach ($line in $lines) { if ($line -match $pattern) { if ($hash.Contains($Matches.key)) { $hash[($Matches.key)] += ',' + $Matches.value } else { $hash.Add($Matches.key, $Matches.value) } } } $result = New-Object -type psobject -prop ($hash) if ($ShowRequiredMetadata) { $result | Select-Object title, 'og:title', description, 'ms.manager', 'ms.author', author, 'ms.service', 'ms.date', 'ms.topic', 'ms.subservice', 'ms.prod', 'ms.technology', 'ms.custom', 'ROBOTS' } else { $result } } function Get-LocaleFreshness { [CmdletBinding()] [OutputType('DocumentLocaleInfo')] param( [Parameter(Mandatory, Position = 0)] [uri]$Uri, [Parameter(Position = 1)] [ValidateScript({$_ -in [LearnLocales]::SupportedLocales})] [string[]]$Locale = [LearnLocales]::CommonLocales ) $localeInUrl = $uri.Segments[1].Trim('/') if ($localeInUrl -notin [LearnLocales]::SupportedLocales) { Write-Error "URL does not contain a supported locale: $localeInUrl" return } else { $url = $uri.OriginalString if ($Locale -notcontains 'en-us') { $Locale += 'en-us' } $Locale | ForEach-Object { $locPath = $_ $result = Get-HtmlMetaTags ($url -replace $localeInUrl, $locPath) | Select-Object @{n = 'locpath'; e = { $locPath } }, locale, 'ms.contentlocale', 'ms.translationtype', 'ms.date', 'loc_version', 'updated_at', 'loc_source_id', 'loc_file_id', 'original_content_git_url' $result.pstypenames.Insert(0, 'DocumentLocaleInfo') $result } | Sort-Object 'updated_at', 'ms.contentlocale' } } function Sync-BeyondCompare { [cmdletbinding()] param ( [Parameter(Mandatory, Position = 0)] [string]$Path ) ### Get-GitStatus comes from the posh-git module. $gitStatus = Get-GitStatus if ($gitStatus) { $reponame = $GitStatus.RepoName } else { Write-Warning 'Not a git repo.' return } $repoPath = $global:git_repos[$reponame].path $ops = Get-Content $repoPath\.openpublishing.publish.config.json | ConvertFrom-Json -Depth 10 -AsHashtable $srcPath = $ops.docsets_to_publish.build_source_folder if ($srcPath -eq '.') { $srcPath = '' } $basePath = Join-Path $repoPath $srcPath '\' $mapPath = Join-Path $basePath $ops.docsets_to_publish.monikerPath $monikers = Get-Content $mapPath | ConvertFrom-Json -Depth 10 -AsHashtable $startPath = (Get-Item $Path).fullname $vlist = $monikers.keys | ForEach-Object { $monikers[$_].packageRoot } if ($startpath) { $relPath = $startPath -replace [regex]::Escape($basepath) $version = ($relPath -split '\\')[0] foreach ($v in $vlist) { if ($v -ne $version) { $target = $startPath -replace [regex]::Escape($version), $v if (Test-Path $target) { Start-Process -Wait "${env:ProgramFiles}\Beyond Compare 4\BComp.exe" -ArgumentList $startpath, $target } } } } else { Write-Error "Invalid path: $Path" } } function Sync-VSCode { [cmdletbinding()] param( [Parameter(Mandatory, Position = 0)] [string]$Path ) ### Get-GitStatus comes from the posh-git module. $gitStatus = Get-GitStatus if ($gitStatus) { $reponame = $GitStatus.RepoName } else { Write-Warning 'Not a git repo.' return } $repoPath = $global:git_repos[$reponame].path $ops = Get-Content $repoPath\.openpublishing.publish.config.json | ConvertFrom-Json -Depth 10 -AsHashtable $srcPath = $ops.docsets_to_publish.build_source_folder if ($srcPath -eq '.') { $srcPath = '' } $basePath = Join-Path $repoPath $srcPath '\' $mapPath = Join-Path $basePath $ops.docsets_to_publish.monikerPath $monikers = Get-Content $mapPath | ConvertFrom-Json -Depth 10 -AsHashtable $startPath = (Get-Item $Path).fullname $vlist = $monikers.keys | ForEach-Object { $monikers[$_].packageRoot } if ($startpath) { $relPath = $startPath -replace [regex]::Escape($basepath) $version = ($relPath -split '\\')[0] foreach ($v in $vlist) { if ($v -ne $version) { $target = $startPath -replace [regex]::Escape($version), $v if (Test-Path $target) { Start-Process -Wait -WindowStyle Hidden 'code' -ArgumentList '--diff', '--wait', '--reuse-window', $startpath, $target } } } } else { Write-Error "Invalid path: $Path" } } function Test-YamlTOC { [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateScript({ Test-Path $_ -PathType Container })] [string]$Path ) $Path = (Resolve-Path $Path).Path # Find the TOC files $tocList = Get-ChildItem $Path -Filter 'toc.yml' -Recurse -File | Where-Object { $_.FullName -notmatch 'bread' } if ($tocList.Count -eq 0) { Write-Warning "No TOC.yml files found in path: $Path" return } # Find all content files $fileList = Get-ChildItem $Path -Include *.md, *.yml -Recurse -File | Where-Object { $_.FullName -notmatch 'bread.+\\toc.yml' } $statusList = foreach ($file in $fileList) { [pscustomobject]@{ FileExists = $true IsInTOC = $false FileName = $file.FullName TOCFileName = '' } } #Process TOC files $hrefPattern = '\s*href:\s+([\w\-\._/]+)\s*$' foreach ($toc in $tocList) { $tocBasePath = Split-Path $toc.FullName -Parent $hrefList = (Select-String -Pattern $hrefPattern -Path $toc.FullName).Matches | ForEach-Object { $_.Groups[1].Value } foreach ($href in $hrefList) { $filePath = Join-Path $tocBasePath $href if ($filePath -in $fileList.FullName) { $statusList.Where({ $_.FileName -eq $filePath }).ForEach({ $_.IsInTOC = $true $_.TOCFileName = $toc.FullName }) } else { $statusList += [pscustomobject]@{ FileExists = $false IsInTOC = $true FileName = $filePath TOCFileName = $toc.FullName } } } } # Output results $statusList | Where-Object { -not $_.FileExists -or -not $_.IsInTOC } } #endregion Functions.Public # Define the types to export with type accelerators. $ExportableTypes =@( [LearnLocales] ) # Get the internal TypeAccelerators class to use its static methods. $TypeAcceleratorsClass = [psobject].Assembly.GetType( 'System.Management.Automation.TypeAccelerators' ) # Ensure none of the types would clobber an existing type accelerator. # If a type accelerator with the same name exists, throw an exception. $ExistingTypeAccelerators = $TypeAcceleratorsClass::Get foreach ($Type in $ExportableTypes) { if ($Type -in $ExistingTypeAccelerators.Keys) { $Message = @( "Unable to register type accelerator '$($Type.FullName)'" 'Accelerator already exists' ) -join ' ' throw [System.Management.Automation.ErrorRecord]::new( [System.InvalidOperationException]::new($Message), 'TypeAcceleratorAlreadyExists', [System.Management.Automation.ErrorCategory]::InvalidOperation, $Type.FullName ) } } # Add the type accelerators for every exportable type. foreach ($Type in $ExportableTypes) { Write-Verbose "Registering type accelerator for [$Type]" $TypeAcceleratorsClass::Add($Type.FullName, $Type) } # Remove type accelerators when the module is removed. $MyInvocation.MyCommand.ScriptBlock.Module.OnRemove = { foreach ($Type in $ExportableTypes) { Write-Verbose "Unregistering type accelerator for [$Type]" $null = $TypeAcceleratorsClass::Remove($Type.FullName) } }.GetNewClosure() $ExportableFunctions = @( 'Export-MonikerContent' 'Get-CmdletXref' 'Get-HtmlMetaTags' 'Get-LocaleFreshness' 'Sync-BeyondCompare' 'Sync-VSCode' 'Test-YamlTOC' ) Export-ModuleMember -Alias * -Function $ExportableFunctions |