Public/New-ConfluenceContentInternalLink.ps1

function New-ConfluenceContentInternalLink {
    <#
    .SYNOPSIS
        Creates a link to a Confluence page, optionally to a heading on that page.

    .DESCRIPTION
        By default New-ConfluenceContentInternalLink returns a Confluence storage-format page link:

            <ac:link><ri:page ri:content-title="Title" ri:space-key="KEY"/><ac:link-body>Text</ac:link-body></ac:link>

        Confluence resolves this link by page title, keeps it working when the page is moved or
        renamed, and shows it as a page link. Give the page with -PageTitle (and -SpaceKey for a page
        in another space), or with -PageId, in which case the title and space key are looked up with
        Get-ConfluencePage (this needs Set-ConfluenceContext). -HeadingLink adds ac:anchor so the link
        points to a heading on the page.

        URL mode (the behaviour before 0.2.0): with -InternalLinkURL, or with -PageId and -AsUrl, an
        <a href> element pointing to the page URL is returned instead. With -HeadingLink the URL gets
        the Confluence anchor "#<PageTitle>-<Heading>" with spaces removed; the page title comes from
        -PageTitle or is looked up from the page ID in the URL.

        All values are escaped, so the result is always well-formed storage format.

    .PARAMETER PageTitle
        The title of the page to link to. With -InternalLinkURL it is only used to build the heading
        anchor.

    .PARAMETER SpaceKey
        The key of the space that contains the page. Leave it out to link to a page in the same space
        as the page that contains the link.

    .PARAMETER InternalLinkURL
        The full URL of the page to link to (URL mode).

    .PARAMETER PageId
        The ID of the page to link to. Its title and space key (or, with -AsUrl, its URL) are looked
        up.

    .PARAMETER AsUrl
        With -PageId, return an <a href> link to the page URL instead of a storage-format page link.

    .PARAMETER HeadingLink
        The heading on the target page to link to.

    .PARAMETER LinkText
        The text shown for the link. Defaults to the page title (URL mode: the URL).

    .EXAMPLE
        New-ConfluenceContentInternalLink -PageTitle 'Runbook' -SpaceKey OPS -HeadingLink 'Restart steps' -LinkText 'Restart steps'

        Returns <ac:link ac:anchor="Restart steps"><ri:page ri:content-title="Runbook" ri:space-key="OPS"/><ac:link-body>Restart steps</ac:link-body></ac:link>.

    .EXAMPLE
        New-ConfluenceContentInternalLink -PageId 123456 -LinkText 'See the runbook'

        Looks up page 123456 and returns a storage-format link to it.

    .EXAMPLE
        New-ConfluenceContentInternalLink -InternalLinkURL 'https://contoso.atlassian.net/wiki/spaces/DOCS/pages/123/Runbook' -PageTitle 'Runbook' -HeadingLink 'Restart steps'

        Returns an <a href> link to the "Restart steps" heading of the Runbook page (URL mode).

    .OUTPUTS
        System.String
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
        Justification = 'Only builds a storage-format string in memory; nothing outside the session is changed.')]
    [CmdletBinding(DefaultParameterSetName = 'PageTitle')]
    [OutputType([string])]
    param (
        [Parameter(Mandatory, HelpMessage = 'Title of the page to link to.', ParameterSetName = 'PageTitle')]
        [Parameter(HelpMessage = 'Title of the page to link to.', ParameterSetName = 'InternalLinkURL')]
        [string]$PageTitle,

        [Parameter(HelpMessage = 'Key of the space that contains the page.', ParameterSetName = 'PageTitle')]
        [string]$SpaceKey,

        [Parameter(Mandatory, HelpMessage = 'URL to the page to link to.', ParameterSetName = 'InternalLinkURL')]
        [string]$InternalLinkURL,

        [Parameter(Mandatory, HelpMessage = 'Page ID to link to.', ParameterSetName = 'InternalLinkPageId')]
        [string]$PageId,

        [Parameter(HelpMessage = 'Return an <a href> link to the page URL.', ParameterSetName = 'InternalLinkPageId')]
        [switch]$AsUrl,

        [Parameter(HelpMessage = 'The heading on the page to link to.')]
        [string]$HeadingLink,

        [Parameter(HelpMessage = 'The text to show for the link.')]
        [string]$LinkText
    )

    $TelemetryArgs = @{
        ModuleName    = $MyInvocation.MyCommand.Module.Name
        ModuleVersion = [string]$MyInvocation.MyCommand.Module.Version
        CommandName   = $MyInvocation.MyCommand.Name
        ExecutionID   = [guid]::NewGuid().ToString()
    }
    Invoke-TelemetryCollection @TelemetryArgs -Stage Start -ClearTimer
    $telemetryFailed = $false
    try {
        $page = $null
        if ($PSCmdlet.ParameterSetName -eq 'InternalLinkPageId') {
            $context = Get-ConfluenceContext
            if (-not $context) {
                Write-Error 'No Confluence context is set. Run Set-ConfluenceContext first.'
                return
            }
            $page = Get-ConfluencePage -PageId $PageId -ErrorAction Stop | Select-Object -First 1
            if (-not $page) {
                Write-Error "Could not find page $PageId."
                return
            }
            $PageTitle = $page.title
        }

        if ($PSCmdlet.ParameterSetName -eq 'PageTitle' -or ($PSCmdlet.ParameterSetName -eq 'InternalLinkPageId' -and -not $AsUrl)) {
            if ($page) {
                # The space key is part of the page's web UI path: /spaces/<KEY>/pages/<id>/...
                $webUi = if ($page._links) { [string]$page._links.webui } else { '' }
                $keyMatch = [regex]::Match($webUi, '/spaces/([^/]+)/')
                if ($keyMatch.Success) { $SpaceKey = [System.Uri]::UnescapeDataString($keyMatch.Groups[1].Value) }
                if (-not $PageTitle) {
                    Write-Error "Could not find the title of page $PageId."
                    return
                }
            }
            $anchor = if ($HeadingLink) { ' ac:anchor="' + (ConvertTo-ConfluenceXmlText -Text $HeadingLink -Attribute) + '"' } else { '' }
            $spaceAttribute = if ($SpaceKey) { ' ri:space-key="' + (ConvertTo-ConfluenceXmlText -Text $SpaceKey -Attribute) + '"' } else { '' }
            $bodyText = if ($LinkText) { $LinkText } else { $PageTitle }
            return ('<ac:link{0}><ri:page ri:content-title="{1}"{2}/><ac:link-body>{3}</ac:link-body></ac:link>' -f
                $anchor, (ConvertTo-ConfluenceXmlText -Text $PageTitle -Attribute), $spaceAttribute, (ConvertTo-ConfluenceXmlText -Text $bodyText))
        }

        # URL mode
        if ($PSCmdlet.ParameterSetName -eq 'InternalLinkURL') {
            $Link = $InternalLinkURL.TrimEnd('/') + '/'
        }
        else {
            $webUi = if ($page._links) { $page._links.webui } else { $null }
            if (-not $webUi) {
                Write-Error "Could not find the web URL of page $PageId."
                return
            }
            $Link = $context.ConnectionBaseURL + '/wiki' + $webUi
        }
        Write-Verbose "Link: $Link"

        if ($HeadingLink) {
            if (-not $PageTitle) {
                $parsedId = [regex]::Match($Link, '/pages/(\d+)(/|$)').Groups[1].Value
                Write-Verbose "Parsed page ID: $parsedId"
                if (-not $parsedId) {
                    Write-Error "Could not find a page ID in '$Link'. Pass -PageTitle to link to a heading."
                    return
                }
                $PageTitle = (Get-ConfluencePage -PageId $parsedId -ErrorAction Stop | Select-Object -First 1).title
            }
            $Link = $Link + '#' + ($PageTitle -replace ' ', '') + '-' + ($HeadingLink -replace ' ', '')
            Write-Verbose "Link: $Link"
        }

        if (-not $LinkText) { $LinkText = $Link }
        return ("<a href='{0}'>{1}</a>" -f (ConvertTo-ConfluenceXmlText -Text $Link -Attribute), (ConvertTo-ConfluenceXmlText -Text $LinkText))
    }
    catch {
        $telemetryFailed = $true
        Invoke-TelemetryCollection @TelemetryArgs -Stage End -Failed $true -Exception $_
        throw
    }
    finally {
        if (-not $telemetryFailed) {
            Invoke-TelemetryCollection @TelemetryArgs -Stage End
        }
    }
}