XurrentHelpers.psm1

#Region './Private/Get-Section.ps1' -1

function Get-Section
{
    <#
        .SYNOPSIS
        Extracts the body of a named Markdown H2 section.

        .DESCRIPTION
        Returns the trimmed text body of a ## Heading section from a Markdown string.
        Matches content from the heading line to the next same-level ## heading or end of string.
        Returns an empty string when the requested section is not found.

        .PARAMETER Content
        The full Markdown document content as a string.

        .PARAMETER Heading
        The section heading to search for, without the leading ## prefix.

        .EXAMPLE
        Get-Section -Content $markdown -Heading 'Description'
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Content,

        [Parameter(Mandatory = $true)]
        [string]
        $Heading
    )

    process
    {
        $pattern = "(?ms)^##\s+$([regex]::Escape($Heading))\s*$\r?\n(.*?)(?=\r?\n^##\s+|\z)"
        if ($Content -match $pattern)
        {
            return $Matches[1].Trim()
        }
        return ''
    }
}
#EndRegion './Private/Get-Section.ps1' 44
#Region './Public/ConvertFrom-XurrentKnowledgeArticle.ps1' -1

function ConvertFrom-XurrentKnowledgeArticle
{
    <#
        .SYNOPSIS
        Converts a Xurrent knowledge article CSV row to a Markdown file.

        .DESCRIPTION
        Takes a PSCustomObject in the Xurrent / 4me bulk-import CSV schema (as produced
        by Import-Csv or ConvertTo-XurrentKnowledgeArticle) and writes a *KnowledgeArticle.md
        file with the correct structure: H1 for Subject, **Keywords:** line, and ## Description
        and ## Instructions sections. Accepts pipeline input for batch processing of CSV rows.
        Invalid filename characters in Subject are replaced with hyphens.

        .PARAMETER InputObject
        A PSCustomObject with Subject, Description, Instructions, and Keywords properties,
        matching the Xurrent knowledge article CSV schema. Accepts pipeline input.

        .PARAMETER Path
        The folder in which to write the Markdown file.
        Defaults to the current working directory.

        .EXAMPLE
        Import-Csv .\knowledge_articles.csv | ConvertFrom-XurrentKnowledgeArticle -Path .\Articles

        .EXAMPLE
        $row = [PSCustomObject]@{ Subject = 'VPN Setup'; Description = 'How to...'; Instructions = 'Step 1...'; Keywords = 'vpn' }
        ConvertFrom-XurrentKnowledgeArticle -InputObject $row -Path .\Output
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.IO.FileInfo])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [PSCustomObject]
        $InputObject,

        [Parameter()]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]
        $Path = (Get-Location).Path
    )

    process
    {
        $subject      = if ($InputObject.Subject)      { [string]$InputObject.Subject }      else { '' }
        $description  = if ($InputObject.Description)  { [string]$InputObject.Description }  else { '' }
        $instructions = if ($InputObject.Instructions) { [string]$InputObject.Instructions } else { '' }
        $keywords     = if ($InputObject.Keywords)     { [string]$InputObject.Keywords }     else { '' }

        $invalidChars = [System.IO.Path]::GetInvalidFileNameChars() -join ''
        $safeName = $subject -replace "[$([regex]::Escape($invalidChars))]", '-'
        $safeName = $safeName.Trim('-', ' ')
        if (-not $safeName) { $safeName = 'Unknown' }

        $fileName = '{0}KnowledgeArticle.md' -f $safeName
        $filePath = Join-Path -Path $Path -ChildPath $fileName

        $content = @"
# $subject

**Keywords:** $keywords

## Description

$description

## Instructions

$instructions
"@


        if ($PSCmdlet.ShouldProcess($filePath, 'Create knowledge article Markdown file'))
        {
            Set-Content -Path $filePath -Value $content -Encoding UTF8
            Get-Item -Path $filePath
        }
    }
}
#EndRegion './Public/ConvertFrom-XurrentKnowledgeArticle.ps1' 79
#Region './Public/ConvertTo-XurrentKnowledgeArticle.ps1' -1

function ConvertTo-XurrentKnowledgeArticle
{
    <#
        .SYNOPSIS
        Converts a Markdown knowledge article file to a Xurrent import object.

        .DESCRIPTION
        Reads a *KnowledgeArticle.md file and extracts the Subject (first H1 heading),
        Description (## Description section), Instructions (## Instructions section),
        and Keywords (**Keywords:** line) into a PSCustomObject formatted for the
        Xurrent / 4me bulk-import CSV schema.

        .PARAMETER File
        The input file to convert. Supports FileInfo, path strings, and objects that
        expose a FullName or Path property. Accepts pipeline input.

        .PARAMETER Service
        The Xurrent service name to write to the Service column of the import row.

        .PARAMETER ServiceInstances
        The Xurrent service instance name(s) for the Service Instances column.

        .EXAMPLE
        Get-Item .\MyAppKnowledgeArticle.md | ConvertTo-XurrentKnowledgeArticle -Service 'techwork automator' -ServiceInstances 'techwork automator for ACS'

        .EXAMPLE
        Get-ChildItem -Recurse -Filter '*KnowledgeArticle.md' | ConvertTo-XurrentKnowledgeArticle -Service 'my svc' -ServiceInstances 'my inst'
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('FullName', 'Path', 'PSPath')]
        [object]
        $File,

        [Parameter(Mandatory = $true)]
        [string]
        $Service,

        [Parameter(Mandatory = $true)]
        [string]
        $ServiceInstances
    )

    process
    {
        $filePath = $null
        if ($File -is [System.IO.FileInfo])
        {
            $filePath = $File.FullName
        }
        elseif ($File -is [string])
        {
            $filePath = $File
        }
        elseif ($null -ne $File.PSObject.Properties['FullName'])
        {
            $filePath = [string]$File.FullName
        }
        elseif ($null -ne $File.PSObject.Properties['Path'])
        {
            $filePath = [string]$File.Path
        }

        if (-not $filePath)
        {
            throw 'File must be a path string, FileInfo, or an object with FullName/Path.'
        }

        $resolvedFile = Get-Item -LiteralPath $filePath -ErrorAction Stop
        if ($resolvedFile -isnot [System.IO.FileInfo])
        {
            throw "Input path '$filePath' is not a file."
        }

        $raw = Get-Content -Path $resolvedFile.FullName -Raw -Encoding UTF8

        $subject = ''
        if ($raw -match '(?m)^#\s+(.+)$') { $subject = $Matches[1].Trim() }

        $keywords = ''
        if ($raw -match '\*\*Keywords:\*\*\s*(.+)') { $keywords = $Matches[1].Trim() }

        [PSCustomObject]@{
            ID                  = ''
            Source              = '4me'
            'Source ID'         = ''
            Status              = 'work_in_progress'
            Service             = $Service
            'Service Instances' = $ServiceInstances
            Subject             = $subject
            Description         = Get-Section -Content $raw -Heading 'Description'
            Instructions        = Get-Section -Content $raw -Heading 'Instructions'
            Keywords            = $keywords
            Template            = ''
        }
    }
}
#EndRegion './Public/ConvertTo-XurrentKnowledgeArticle.ps1' 101
#Region './Public/Export-XurrentKnowledgeArticle.ps1' -1

function Export-XurrentKnowledgeArticle
{
    <#
        .SYNOPSIS
        Exports Xurrent knowledge articles from Markdown files or objects to a CSV import file.

        .DESCRIPTION
        Accepts either a folder path (scanned recursively for *KnowledgeArticle.md files) or an
        array of pre-built article objects (e.g. the output of ConvertTo-XurrentKnowledgeArticle)
        and writes a CSV in the Xurrent / 4me bulk-import format.

        When using the Folder parameter set, Subject is read from the first H1 heading,
        Description and Instructions from same-named ## sections, Keywords from a
        **Keywords:** line. Service and ServiceInstances can be set via parameters or
        loaded from a .env file (SERVICE and SERVICE_INSTANCES keys).

        When using the InputObject parameter set the objects are exported as-is; Service,
        ServiceInstances, and EnvFile parameters are not applicable.

        .PARAMETER Folder
        Path to the folder to scan recursively for *KnowledgeArticle.md files.

        .PARAMETER InputObject
        One or more knowledge article objects (as returned by ConvertTo-XurrentKnowledgeArticle)
        to export directly without scanning a folder. Accepts pipeline input.

        .PARAMETER Service
        Xurrent service name written to the Service column of every export row.
        Defaults to 'techwork automator'. Can also be supplied via SERVICE= in a .env file.

        .PARAMETER ServiceInstances
        Xurrent service instance name(s) for the Service Instances column.
        Can be supplied via SERVICE_INSTANCES= in a .env file; prompts interactively if still empty.

        .PARAMETER OutputPath
        Full path of the CSV file to write.
        Defaults to import-knowledge_articles.csv in the current working directory.

        .PARAMETER EnvFile
        Path to a .env file supplying SERVICE and SERVICE_INSTANCES defaults.
        Defaults to .env in the current working directory when that file exists.

        .EXAMPLE
        Export-XurrentKnowledgeArticle -Folder .\ACS -Service 'techwork automator' -ServiceInstances 'techwork automator for ACS'

        .EXAMPLE
        Export-XurrentKnowledgeArticle -Folder .\TTTech
        Loads SERVICE and SERVICE_INSTANCES from .env in the current directory.

        .EXAMPLE
        Get-ChildItem -Recurse -Filter '*KnowledgeArticle.md' |
            ConvertTo-XurrentKnowledgeArticle -Service 'techwork automator' -ServiceInstances 'ACS' |
            Export-XurrentKnowledgeArticle -OutputPath .\out.csv
        Pipes pre-converted article objects directly into the export.

        .EXAMPLE
        $articles = ConvertTo-XurrentKnowledgeArticle -File .\MyArticleKnowledgeArticle.md -Service 'svc' -ServiceInstances 'inst'
        Export-XurrentKnowledgeArticle -InputObject $articles
        Passes an object array directly without pipeline.
    #>

    [CmdletBinding(SupportsShouldProcess = $true, DefaultParameterSetName = 'FromFolder')]
    param
    (
        [Parameter(Mandatory = $true, ParameterSetName = 'FromFolder')]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]
        $Folder,

        [Parameter(Mandatory = $true, ParameterSetName = 'FromObjects', ValueFromPipeline = $true)]
        [object[]]
        $InputObject,

        [Parameter(ParameterSetName = 'FromFolder')]
        [string]
        $Service = 'techwork automator',

        [Parameter(ParameterSetName = 'FromFolder')]
        [string]
        $ServiceInstances = '',

        [Parameter()]
        [string]
        $OutputPath = (Join-Path (Get-Location).Path 'import-knowledge_articles.csv'),

        [Parameter(ParameterSetName = 'FromFolder')]
        [string]
        $EnvFile = (Join-Path (Get-Location).Path '.env')
    )

    begin
    {
        $rows = [System.Collections.Generic.List[object]]::new()
    }

    process
    {
        if ($PSCmdlet.ParameterSetName -eq 'FromObjects')
        {
            foreach ($obj in $InputObject) { $rows.Add($obj) }
            return
        }

        if (Test-Path $EnvFile)
        {
            foreach ($line in (Get-Content $EnvFile))
            {
                if ($line -match "^([A-Z_]+)\s*=\s*'?([^']*?)'?\s*$")
                {
                    $key = $Matches[1]
                    $val = $Matches[2]
                    if ($key -eq 'SERVICE' -and -not $PSBoundParameters.ContainsKey('Service')) { $Service = $val }
                    if ($key -eq 'SERVICE_INSTANCES' -and -not $PSBoundParameters.ContainsKey('ServiceInstances')) { $ServiceInstances = $val }
                }
            }
        }

        if (-not $Service)
        {
            $Service = Read-Host -Prompt "Enter SERVICE name (e.g. 'techwork automator')"
        }
        if (-not $ServiceInstances)
        {
            $ServiceInstances = Read-Host -Prompt 'Enter SERVICE_INSTANCES'
        }

        $files = Get-ChildItem -Path $Folder -Recurse -Filter '*KnowledgeArticle.md'

        if ($files.Count -eq 0)
        {
            Write-Warning "No *KnowledgeArticle.md files found in $Folder"
            return
        }

        Write-Verbose "Found $($files.Count) knowledge article(s) in $Folder"

        foreach ($file in $files)
        {
            Write-Verbose " Processing $($file.FullName)"
            $rows.Add((ConvertTo-XurrentKnowledgeArticle -File $file -Service $Service -ServiceInstances $ServiceInstances))
        }
    }

    end
    {
        if ($rows.Count -eq 0) { return }

        if ($PSCmdlet.ShouldProcess($OutputPath, 'Export CSV'))
        {
            $encoding = if ($PSVersionTable.PSVersion.Major -ge 7) { 'utf8BOM' } else { 'UTF8' }
            $rows | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding $encoding
            Write-Verbose "Exported $($rows.Count) article(s) to $OutputPath"
        }
    }
}
#EndRegion './Public/Export-XurrentKnowledgeArticle.ps1' 155
#Region './Public/Import-XurrentKnowledgeArticle.ps1' -1

function Import-XurrentKnowledgeArticle
{
    <#
        .SYNOPSIS
        Imports Xurrent knowledge articles from a CSV export file and writes Markdown files.

        .DESCRIPTION
        Reads a CSV file in the Xurrent / 4me bulk-import format and writes one
        *KnowledgeArticle.md file per row to the specified output folder. Subject becomes
        the H1 heading and the filename prefix, Description and Instructions populate their
        respective ## sections, and Keywords are written to a **Keywords:** line. Passes
        -WhatIf through to ConvertFrom-XurrentKnowledgeArticle for dry-run support.

        .PARAMETER CsvPath
        Path to the Xurrent knowledge article CSV export file to read.

        .PARAMETER OutputFolder
        The folder in which to write the Markdown files.
        Defaults to the current working directory.

        .EXAMPLE
        Import-XurrentKnowledgeArticle -CsvPath .\export-knowledge_articles.csv -OutputFolder .\Articles

        .EXAMPLE
        Import-XurrentKnowledgeArticle -CsvPath .\export.csv
        Writes Markdown files to the current working directory.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.IO.FileInfo])]
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]
        $CsvPath,

        [Parameter()]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]
        $OutputFolder = (Get-Location).Path
    )

    process
    {
        $rows = @(Import-Csv -Path $CsvPath -Encoding UTF8)

        if ($rows.Count -eq 0)
        {
            Write-Warning "No rows found in $CsvPath"
            return
        }

        Write-Verbose "Found $($rows.Count) row(s) in $CsvPath"

        if ($PSCmdlet.ShouldProcess($CsvPath, 'Import knowledge articles to Markdown'))
        {
            foreach ($row in $rows)
            {
                Write-Verbose " Processing '$($row.Subject)'"
                ConvertFrom-XurrentKnowledgeArticle -InputObject $row -Path $OutputFolder
            }
        }
    }
}
#EndRegion './Public/Import-XurrentKnowledgeArticle.ps1' 65
#Region './Public/New-XurrentKnowledgeArticleCsvExample.ps1' -1

function New-XurrentKnowledgeArticleCsvExample
{
    <#
        .SYNOPSIS
        Creates an example CSV file in the Xurrent knowledge article import format.

        .DESCRIPTION
        Writes a CSV file with all columns required by the Xurrent / 4me bulk-import schema
        for knowledge articles, pre-filled with one illustrative example row. Use it as a
        reference or starting point before populating real data via Export-KnowledgeArticle.

        .PARAMETER OutputPath
        Full path of the CSV file to write.
        Defaults to example-knowledge_articles.csv in the current working directory.

        .EXAMPLE
        New-KnowledgeArticleCsvExample
        Creates example-knowledge_articles.csv in the current directory.

        .EXAMPLE
        New-KnowledgeArticleCsvExample -OutputPath C:\Temp\example.csv
        Creates the example CSV at the specified path.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.IO.FileInfo])]
    param
    (
        [Parameter()]
        [string]
        $OutputPath = (Join-Path (Get-Location).Path 'example-knowledge_articles.csv')
    )

    process
    {
        $example = [PSCustomObject]@{
            ID                  = ''
            Source              = '4me'
            'Source ID'         = ''
            Status              = 'work_in_progress'
            Service             = 'my service'
            'Service Instances' = 'my service instance'
            Subject             = 'Example knowledge article subject'
            Description         = 'A brief description of the knowledge article.'
            Instructions        = 'Step 1: Do this. Step 2: Do that.'
            Keywords            = 'example, keyword1, keyword2'
            Template            = ''
        }

        if ($PSCmdlet.ShouldProcess($OutputPath, 'Create example knowledge article CSV'))
        {
            $encoding = if ($PSVersionTable.PSVersion.Major -ge 7) { 'utf8BOM' } else { 'UTF8' }
            $example | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding $encoding
            Get-Item -Path $OutputPath
        }
    }
}
#EndRegion './Public/New-XurrentKnowledgeArticleCsvExample.ps1' 57
#Region './Public/New-XurrentKnowledgeArticleTemplate.ps1' -1

function New-XurrentKnowledgeArticleTemplate
{
    <#
        .SYNOPSIS
        Creates a Markdown template file for a Xurrent knowledge article.

        .DESCRIPTION
        Writes a *KnowledgeArticle.md template to the specified folder with the correct
        structure expected by ConvertTo-KnowledgeArticle: an H1 heading for the Subject,
        a **Keywords:** line, and ## Description and ## Instructions sections.

        .PARAMETER Name
        The name of the knowledge article, used as the H1 heading and filename prefix.
        The file is written as <Name>KnowledgeArticle.md.

        .PARAMETER Path
        The folder in which to create the template file.
        Defaults to the current working directory.

        .EXAMPLE
        New-KnowledgeArticleTemplate -Name 'HowToResetPassword'
        Creates HowToResetPasswordKnowledgeArticle.md in the current directory.

        .EXAMPLE
        New-KnowledgeArticleTemplate -Name 'VPN Setup' -Path C:\Articles
        Creates 'VPN SetupKnowledgeArticle.md' in C:\Articles.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    [OutputType([System.IO.FileInfo])]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]
        $Name,

        [Parameter()]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]
        $Path = (Get-Location).Path
    )

    process
    {
        $fileName = '{0}KnowledgeArticle.md' -f $Name
        $filePath = Join-Path -Path $Path -ChildPath $fileName

        $template = @"
# $Name

**Keywords:** keyword1, keyword2

## Description

Describe the knowledge article here.

## Instructions

Provide step-by-step instructions here.
"@


        if ($PSCmdlet.ShouldProcess($filePath, 'Create knowledge article template'))
        {
            Set-Content -Path $filePath -Value $template -Encoding UTF8
            Get-Item -Path $filePath
        }
    }
}
#EndRegion './Public/New-XurrentKnowledgeArticleTemplate.ps1' 68