@@ -35,13 +35,145 @@ function Invoke-CheckedCommand {
3535 }
3636}
3737
38+ # Online fallbacks for links whose target is not shipped in a bundle.
39+ $onlineDocsBaseUrl = ' https://scriptedevents.github.io/ScriptedEventsReloaded/'
40+ $repositoryWebUrl = ' https://github.com/ScriptedEvents/ScriptedEventsReloaded'
41+ $repositoryRawUrl = ' https://raw.githubusercontent.com/ScriptedEvents/ScriptedEventsReloaded/main'
42+ $markdownLinkPattern = ' (?<prefix>\]\(|\bsrc=")(?<target>[^)"\s]+)(?<suffix>[)"])'
43+
44+ function ConvertTo-UrlPath {
45+ param ([Parameter (Mandatory )] [AllowEmptyString ()] [string ]$RelativePath )
46+
47+ (($RelativePath -split ' [\\/]' ) | ForEach-Object { [System.Uri ]::EscapeDataString($_ ) }) -join ' /'
48+ }
49+
50+ function Test-IsRelativeLinkTarget {
51+ param ([Parameter (Mandatory )] [string ]$Target )
52+
53+ # Skip web/mail links, in-page anchors and site-root paths.
54+ return -not ($Target -match ' ^(?:[a-zA-Z][a-zA-Z0-9+.-]*:|#|/)' )
55+ }
56+
57+ function Split-LinkTarget {
58+ param ([Parameter (Mandatory )] [string ]$Target )
59+
60+ $hashIndex = $Target.IndexOf (' #' )
61+ if ($hashIndex -lt 0 ) {
62+ return @ ([System.Uri ]::UnescapeDataString($Target ), ' ' )
63+ }
64+ return @ ([System.Uri ]::UnescapeDataString($Target.Substring (0 , $hashIndex )), $Target.Substring ($hashIndex ))
65+ }
66+
67+ function Get-OnlineLinkTarget {
68+ param (
69+ [Parameter (Mandatory )] [string ]$RepositoryRelativePath ,
70+ [switch ]$RawFile
71+ )
72+
73+ $path = $RepositoryRelativePath -replace ' \\' , ' /'
74+ if ($path -match ' ^docs/(?<page>.+)\.md$' ) {
75+ $page = $Matches [' page' ]
76+ if ($page -eq ' README' ) {
77+ return $onlineDocsBaseUrl
78+ }
79+ if ($page.EndsWith (' /README' )) {
80+ $page = $page.Substring (0 , $page.Length - ' README' .Length)
81+ }
82+ return $onlineDocsBaseUrl + (ConvertTo-UrlPath $page )
83+ }
84+ if ($RawFile ) {
85+ return " $repositoryRawUrl /$ ( ConvertTo-UrlPath $path ) "
86+ }
87+ $kind = if (Test-Path - LiteralPath (Join-Path $repositoryDirectory $path ) - PathType Container) { ' tree' } else { ' blob' }
88+ return " $repositoryWebUrl /$kind /main/$ ( ConvertTo-UrlPath $path ) "
89+ }
90+
91+ # Copies a repository Markdown file to the root of a bundle. Relative links are
92+ # pointed at the matching file inside that bundle when it is shipped there, and
93+ # at the online documentation or GitHub repository when it is not.
94+ function Copy-MarkdownToBundleRoot {
95+ param (
96+ [Parameter (Mandatory )] [string ]$RepositoryRelativePath ,
97+ [Parameter (Mandatory )] [string ]$BundleRoot
98+ )
99+
100+ $sourceFile = Join-Path $repositoryDirectory $RepositoryRelativePath
101+ $sourceDirectory = Split-Path - Parent $sourceFile
102+ $repositoryPrefix = $repositoryDirectory.TrimEnd (' \' , ' /' ) + [System.IO.Path ]::DirectorySeparatorChar
103+ $content = [System.IO.File ]::ReadAllText($sourceFile )
104+ $output = New-Object System.Text.StringBuilder
105+ $position = 0
106+
107+ foreach ($match in [regex ]::Matches($content , $markdownLinkPattern )) {
108+ $target = $match.Groups [' target' ].Value
109+ $newTarget = $target
110+ if (Test-IsRelativeLinkTarget $target ) {
111+ $pathPart , $anchor = Split-LinkTarget $target
112+ $fullPath = [System.IO.Path ]::GetFullPath((Join-Path $sourceDirectory $pathPart ))
113+ if (-not $fullPath.StartsWith ($repositoryPrefix , [System.StringComparison ]::OrdinalIgnoreCase)) {
114+ throw " $RepositoryRelativePath links outside the repository: $target "
115+ }
116+ $relativeTarget = $fullPath.Substring ($repositoryPrefix.Length ).TrimEnd(' \' , ' /' )
117+ if (Test-Path - LiteralPath (Join-Path $BundleRoot $relativeTarget )) {
118+ $newTarget = (ConvertTo-UrlPath $relativeTarget ) + $anchor
119+ }
120+ else {
121+ $isImage = $match.Groups [' prefix' ].Value -ne ' ]('
122+ $newTarget = (Get-OnlineLinkTarget - RepositoryRelativePath $relativeTarget - RawFile:$isImage ) + $anchor
123+ }
124+ }
125+
126+ $targetGroup = $match.Groups [' target' ]
127+ [void ]$output.Append ($content , $position , $targetGroup.Index - $position )
128+ [void ]$output.Append ($newTarget )
129+ $position = $targetGroup.Index + $targetGroup.Length
130+ }
131+ [void ]$output.Append ($content , $position , $content.Length - $position )
132+
133+ $destination = Join-Path $BundleRoot (Split-Path - Leaf $sourceFile )
134+ [System.IO.File ]::WriteAllText($destination , $output.ToString (), (New-Object System.Text.UTF8Encoding($false )))
135+ }
136+
137+ # Fails the release if any Markdown file in a bundle links to a local file that
138+ # is missing from that bundle.
139+ function Assert-BundleMarkdownLinks {
140+ param ([Parameter (Mandatory )] [string ]$BundleRoot )
141+
142+ $bundlePrefix = [System.IO.Path ]::GetFullPath($BundleRoot ).TrimEnd(' \' , ' /' ) + [System.IO.Path ]::DirectorySeparatorChar
143+ $brokenLinks = New-Object System.Collections.Generic.List[string ]
144+ foreach ($file in Get-ChildItem - LiteralPath $BundleRoot - Filter ' *.md' - File - Recurse) {
145+ $content = [System.IO.File ]::ReadAllText($file.FullName )
146+ foreach ($match in [regex ]::Matches($content , $markdownLinkPattern )) {
147+ $target = $match.Groups [' target' ].Value
148+ if (-not (Test-IsRelativeLinkTarget $target )) {
149+ continue
150+ }
151+ $pathPart = (Split-LinkTarget $target )[0 ]
152+ if ($pathPart.Length -eq 0 ) {
153+ continue
154+ }
155+ $fullPath = [System.IO.Path ]::GetFullPath((Join-Path $file.DirectoryName $pathPart ))
156+ if (-not $fullPath.StartsWith ($bundlePrefix , [System.StringComparison ]::OrdinalIgnoreCase) -or
157+ -not (Test-Path - LiteralPath $fullPath )) {
158+ $brokenLinks.Add (" $ ( $file.FullName.Substring ($bundlePrefix.Length )) : $target " )
159+ }
160+ }
161+ }
162+
163+ if ($brokenLinks.Count -gt 0 ) {
164+ throw " Bundle $BundleRoot has links to files it does not contain:`n $ ( $brokenLinks -join " `n " ) "
165+ }
166+ }
167+
38168function Copy-ReleaseNotices {
39169 param ([Parameter (Mandatory )] [string ]$Destination )
40170
41171 Copy-Item - LiteralPath (Join-Path $repositoryDirectory ' LICENSE' ) - Destination $Destination
42172 Copy-Item - LiteralPath (Join-Path $repositoryDirectory ' THIRD_PARTY_LICENSES.txt' ) - Destination $Destination
43- Copy-Item - LiteralPath (Join-Path $repositoryDirectory ' README.md' ) - Destination $Destination
44- Copy-Item - LiteralPath (Join-Path $repositoryDirectory ' docs\getting-started\installation.md' ) - Destination $Destination
173+ # Copy these last, after the bundle's other content, so their links can be
174+ # pointed at files that are actually in the bundle.
175+ Copy-MarkdownToBundleRoot - RepositoryRelativePath ' README.md' - BundleRoot $Destination
176+ Copy-MarkdownToBundleRoot - RepositoryRelativePath ' docs\getting-started\installation.md' - BundleRoot $Destination
45177}
46178
47179function Compress-ReleaseDirectory {
@@ -123,6 +255,10 @@ $vsixPath = Join-Path $releaseDirectory "ser-$extensionVersion.vsix"
123255Invoke-CheckedCommand - FilePath ' npm' - Arguments @ (' run' , ' package' , ' --' , ' --out' , $vsixPath ) - WorkingDirectory $extensionDirectory
124256Copy-Item - LiteralPath $vsixPath - Destination (Join-Path $completeStage ' VS Code Extension' )
125257
258+ foreach ($stage in @ ($learningStage , $editorStage , $completeStage )) {
259+ Assert-BundleMarkdownLinks - BundleRoot $stage
260+ }
261+
126262Compress-ReleaseDirectory - Source $learningStage - Destination (Join-Path $releaseDirectory " SER-$coreVersion -Examples-and-Documentation.zip" )
127263Compress-ReleaseDirectory - Source $editorStage - Destination (Join-Path $releaseDirectory " SER-Visual-Editor-$coreVersion .zip" )
128264Compress-ReleaseDirectory - Source $completeStage - Destination (Join-Path $releaseDirectory " SER-$coreVersion -Complete.zip" )
0 commit comments