├── .gitattributes ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── GitVersion.yml ├── LICENSE ├── PSModule.build.ps1 ├── PowerHTML.psd1 ├── PowerHTML.psm1 ├── Private └── .placeholder ├── Public └── ConvertFrom-HTML.ps1 ├── README.md ├── Tests ├── 00-PowershellModule.Tests.ps1 └── PowerHTML.Tests.ps1 ├── Types └── HtmlAgilityPack.HtmlTextNode.ps1xml └── lib ├── HtmlAgilityPack-1.11.60-net40-client.dll ├── HtmlAgilityPack-1.11.60-netstandard2.dll └── HtmlAgilityPack-1.11.60.LICENSE /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /BuildOutput/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "PowerShell", 9 | "request": "launch", 10 | "name": "PowerShell Launch Current File", 11 | "script": "${file}", 12 | "args": [], 13 | "cwd": "${file}" 14 | }, 15 | { 16 | "type": "PowerShell", 17 | "request": "launch", 18 | "name": "PowerShell Launch Current File in Temporary Console", 19 | "script": "${file}", 20 | "args": [], 21 | "cwd": "${file}", 22 | "createTemporaryIntegratedConsole": true 23 | }, 24 | { 25 | "type": "PowerShell", 26 | "request": "launch", 27 | "name": "PowerShell Launch Current File w/Args Prompt", 28 | "script": "${file}", 29 | "args": [ 30 | "${command:SpecifyScriptArgs}" 31 | ], 32 | "cwd": "${file}" 33 | }, 34 | { 35 | "type": "PowerShell", 36 | "request": "attach", 37 | "name": "PowerShell Attach to Host Process", 38 | "processId": "${command:PickPSHostProcess}", 39 | "runspaceId": 1 40 | }, 41 | { 42 | "type": "PowerShell", 43 | "request": "launch", 44 | "name": "PowerShell Interactive Session", 45 | "cwd": "${workspaceRoot}" 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // When enabled, will trim trailing whitespace when you save a file. 3 | "files.trimTrailingWhitespace": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${relativeFile}: the current opened file relative to workspaceRoot 5 | // ${fileBasename}: the current opened file's basename 6 | // ${fileDirname}: the current opened file's dirname 7 | // ${fileExtname}: the current opened file's extension 8 | // ${cwd}: the current working directory of the spawned process 9 | { 10 | // See https://go.microsoft.com/fwlink/?LinkId=733558 11 | // for the documentation about the tasks.json format 12 | "version": "2.0.0", 13 | 14 | // Start PowerShell 15 | "windows": { 16 | "command": "powershell.exe", 17 | "args": [ 18 | "-NoProfile", 19 | "-NonInteractive", 20 | "-ExecutionPolicy", "Bypass" 21 | ], 22 | }, 23 | "linux": { 24 | "command": "pwsh", 25 | "args": [ 26 | "-NoProfile", 27 | "-NonInteractive", 28 | "-ExecutionPolicy", "Bypass" 29 | ], 30 | }, 31 | "osx": { 32 | "command": "pwsh", 33 | "args": [ 34 | "-NoProfile", 35 | "-NonInteractive", 36 | "-ExecutionPolicy", "Bypass" 37 | ], 38 | }, 39 | 40 | // Associate with test task runner 41 | "tasks": [ 42 | { 43 | "label": "Test", 44 | 45 | "group": { 46 | "kind": "test", 47 | "isDefault": true 48 | }, 49 | "windows": { 50 | "command": "powershell.exe" 51 | }, 52 | "osx": { 53 | "command": "pwsh" 54 | }, 55 | "linux": { 56 | "command": "pwsh" 57 | }, 58 | "args": [ 59 | "-NoProfile", 60 | "-NonInteractive", 61 | "-ExecutionPolicy", 62 | "Bypass", 63 | "-Command", 64 | "Invoke-Build -Verbose Test" 65 | ], 66 | "problemMatcher": "$pester" 67 | }, 68 | { 69 | "label": "Test-PSCore", 70 | "group": "test", 71 | "command": "pwsh", 72 | "args": [ 73 | "-NoProfile", 74 | "-NonInteractive", 75 | "-ExecutionPolicy", 76 | "Bypass", 77 | "-Command", 78 | "Invoke-Build -Verbose Test" 79 | ], 80 | "problemMatcher": "$pester" 81 | }, 82 | { 83 | "label": "Build", 84 | "group": { 85 | "kind": "build", 86 | "isDefault": true 87 | }, 88 | "windows": { 89 | "command": "powershell.exe" 90 | }, 91 | "osx": { 92 | "command": "pwsh" 93 | }, 94 | "linux": { 95 | "command": "pwsh" 96 | }, 97 | "args": [ 98 | "-NoProfile", 99 | "-NonInteractive", 100 | "-ExecutionPolicy", 101 | "Bypass", 102 | "-Command", 103 | "Invoke-Build" 104 | ], 105 | "problemMatcher": "$pester" 106 | }, 107 | { 108 | "label": "Build-on-PSCore", 109 | "group": "build", 110 | "command": "pwsh", 111 | "args": [ 112 | "-NoProfile", 113 | "-NonInteractive", 114 | "-ExecutionPolicy", 115 | "Bypass", 116 | "-Command", 117 | "Invoke-Build Build" 118 | ], 119 | "problemMatcher": "$pester" 120 | } 121 | ] 122 | } -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinGrote/PowerHTML/a74a655cee4c979e564c945d5b4aa076e62e887f/GitVersion.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 JustinGrote 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /PSModule.build.ps1: -------------------------------------------------------------------------------- 1 | #requires -version 5 2 | #Build Script for Powershell Modules 3 | #Uses Invoke-Build (https://github.com/nightroman/Invoke-Build) 4 | #Run by changing to the project root directory and run ./Invoke-Build.ps1 5 | #Uses a master-always-deploys strategy and semantic versioning - http://nvie.com/posts/a-successful-git-branching-model/ 6 | 7 | param ( 8 | #Skip publishing to various destinations (Appveyor,Github,PowershellGallery,etc.) 9 | [Switch]$SkipPublish, 10 | #Force deployment step even if we are not in master. If you are following GitFlow or GitHubFlow you should never need to do this. 11 | [Switch]$ForceDeploy, 12 | #Show detailed environment variables 13 | [Switch]$ShowEnvironmentVariables, 14 | #Powershell modules required for the build process 15 | [String[]]$BuildHelperModules = @("BuildHelpers","Pester","powershell-yaml","Microsoft.Powershell.Archive","PSScriptAnalyzer"), 16 | #Which build files/folders should be excluded from packaging 17 | [String[]]$BuildFilesToExclude = @("Build","BuildOutput","Tests",".git*","appveyor.yml","gitversion.yml","*.build.ps1",".vscode",".placeholder"), 18 | #NuGet API Key for Powershell Gallery Deployment. Defaults to environment variable of the same name 19 | [String]$NuGetAPIKey = $env:NuGetAPIKey, 20 | #GitHub User for Github Releases. Defaults to environment variable of the same name 21 | [String]$GitHubUserName = $env:GitHubAPIKey, 22 | #GitHub API Key for Github Releases. Defaults to environment variable of the same name 23 | [String]$GitHubAPIKey = $env:GitHubAPIKey 24 | ) 25 | 26 | #Initialize Build Environment 27 | Enter-Build { 28 | #Initialize Script-scope variables 29 | $ArtifactPaths = @() 30 | $ProjectBuildVersion = $null 31 | $ProjectBuildPath = $null 32 | 33 | $lines = '----------------------------------------------------------------' 34 | function Write-VerboseHeader ([String]$Message) { 35 | #Simple function to add lines around a header 36 | write-verbose "" 37 | write-verbose $lines 38 | write-verbose $Message 39 | write-verbose $lines 40 | } 41 | 42 | #Detect if we are in a continuous integration environment (Appveyor, etc.) or otherwise running noninteractively 43 | if ($ENV:CI -or ([Environment]::GetCommandLineArgs() -like '-noni*')) { 44 | write-build Green 'Build Initialization: Detected a Noninteractive or CI environment, disabling prompt confirmations' 45 | $SCRIPT:CI = $true 46 | $ConfirmPreference = 'None' 47 | $ProgressPreference = "SilentlyContinue" 48 | } 49 | 50 | #Fetch Build Helper Modules using Install-ModuleBootstrap script (works in PSv3/4) 51 | #The comma in ArgumentList a weird idiosyncracy to make sure a nested array is created to ensure Argumentlist 52 | #doesn't unwrap the buildhelpermodules as individual arguments 53 | #We suppress verbose output for master builds (because they should have already been built once cleanly) 54 | 55 | foreach ($BuildHelperModuleItem in $BuildHelperModules) { 56 | if (-not (Get-module $BuildHelperModuleItem -listavailable)) { 57 | write-verbose "Build Initialization: Installing $BuildHelperModuleItem from Powershell Gallery to your currentuser module directory" 58 | if ($PSVersionTable.PSVersion.Major -lt 5) { 59 | write-verboseheader "Bootstrapping Powershell Module: $BuildHelperModuleItem" 60 | Invoke-Command -ArgumentList @(, $BuildHelperModules) -ScriptBlock ([scriptblock]::Create((new-object net.webclient).DownloadString('https://git.io/PSModBootstrap'))) 61 | } else { 62 | $installModuleParams = @{ 63 | Scope = "CurrentUser" 64 | Name = $BuildHelperModuleItem 65 | ErrorAction = "Stop" 66 | } 67 | if ($SCRIPT:CI) { 68 | $installModuleParams.Force = $true 69 | } 70 | install-module @installModuleParams 71 | } 72 | } 73 | } 74 | 75 | #Initialize helpful build environment variables 76 | $Timestamp = Get-date -uformat "%Y%m%d-%H%M%S" 77 | $PSVersion = $PSVersionTable.PSVersion.Major 78 | Set-BuildEnvironment -force 79 | 80 | $PassThruParams = @{} 81 | 82 | #If the branch name is master-test, run the build like we are in "master" 83 | if ($env:BHBranchName -eq 'master-test') { 84 | write-build Magenta "Detected master-test branch, running as if we were master" 85 | $SCRIPT:BranchName = "master" 86 | } else { 87 | $SCRIPT:BranchName = $env:BHBranchName 88 | } 89 | write-build Green "Current Branch Name: $BranchName" 90 | 91 | if ( ($VerbosePreference -ne 'SilentlyContinue') -or ($CI -and ($BranchName -ne 'master')) ) { 92 | write-build Green "Build Initialization: Verbose Build Logging Enabled" 93 | $SCRIPT:VerbosePreference = "Continue" 94 | $PassThruParams.Verbose = $true 95 | } 96 | 97 | 98 | write-verboseheader "Build Environment Prepared! Environment Information:" 99 | Get-BuildEnvironment | format-list | out-string | write-verbose 100 | if ($ShowEnvironmentVariables) { 101 | write-verboseheader "Current Environment Variables" 102 | get-childitem env: | out-string | write-verbose 103 | 104 | write-verboseheader "Powershell Variables" 105 | Get-Variable | select-object name, value, visibility | format-table -autosize | out-string | write-verbose 106 | } 107 | 108 | # #Register Nuget 109 | # if (!(get-packageprovider "Nuget" -ForceBootstrap -ErrorAction silentlycontinue)) { 110 | # write-verbose "Nuget Provider Not found. Fetching..." 111 | # Install-PackageProvider Nuget -forcebootstrap -scope currentuser @PassThruParams | out-string | write-verbose 112 | # write-verboseheader "Installed Nuget Provider Info" 113 | # Get-PackageProvider Nuget @PassThruParams | format-list | out-string | write-verbose 114 | # } 115 | 116 | # #Fix a bug with the Appveyor 2017 image having a broken nuget (points to v3 URL but installed packagemanagement doesn't query v3 correctly) 117 | # #Next command will add this back 118 | # if ($ENV:APPVEYOR -and ($ENV:APPVEYOR_BUILD_WORKER_IMAGE -eq 'Visual Studio 2017')) { 119 | # write-verbose "Detected Appveyor VS2017 Image, using v2 Nuget API" 120 | # UnRegister-PackageSource -Name nuget.org 121 | # } 122 | 123 | # #Add the nuget repository so we can download things like GitVersion 124 | # if (!(Get-PackageSource "nuget.org" -erroraction silentlycontinue)) { 125 | # write-verbose "Registering nuget.org as package source" 126 | # Register-PackageSource -provider NuGet -name nuget.org -location http://www.nuget.org/api/v2 -Trusted @PassThruParams | out-string | write-verbose 127 | # } 128 | # else { 129 | # $nugetOrgPackageSource = Set-PackageSource -name 'nuget.org' -Trusted @PassThruParams 130 | # if ($PassThruParams.Verbose) { 131 | # write-verboseheader "Nuget.Org Package Source Info " 132 | # $nugetOrgPackageSource | format-table | out-string | write-verbose 133 | # } 134 | # } 135 | 136 | #Move to the Project Directory if we aren't there already 137 | Set-Location $buildRoot 138 | 139 | #Define the Project Build Path 140 | $SCRIPT:ProjectBuildPath = $ENV:BHBuildOutput + "\" + $ENV:BHProjectName 141 | Write-Build Green "Module Build Output Path: $ProjectBuildPath" 142 | 143 | #Force TLS 1.2 for all HTTPS transactions 144 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 145 | } 146 | 147 | task Clean { 148 | #Reset the BuildOutput Directory 149 | if (test-path $env:BHBuildOutput) { 150 | Write-Verbose "Removing and resetting Build Output Path: $($ENV:BHBuildOutput)" 151 | remove-item $env:BHBuildOutput -Recurse -Force @PassThruParams 152 | } 153 | New-Item -ItemType Directory $ProjectBuildPath -force | ForEach-Object FullName | out-string | write-verbose 154 | #Unmount any modules named the same as our module 155 | 156 | } 157 | 158 | task Version { 159 | #This task determines what version number to assign this build 160 | $GitVersionConfig = "$buildRoot/GitVersion.yml" 161 | 162 | #Fetch GitVersion 163 | #TODO: Use Nuget.exe to fetch to make this v3/v4 compatible 164 | $GitVersionCMDPackageName = "gitversion.commandline" 165 | $GitVersionCMDPackage = Get-Package $GitVersionCMDPackageName -erroraction SilentlyContinue 166 | if (!($GitVersionCMDPackage)) { 167 | write-verbose "Package $GitVersionCMDPackageName Not Found Locally, Installing..." 168 | write-verboseheader "Nuget.Org Package Source Info for fetching GitVersion" 169 | Get-PackageSource | Format-Table | out-string | write-verbose 170 | 171 | #Fetch GitVersion 172 | $GitVersionCMDPackage = Install-Package $GitVersionCMDPackageName -scope currentuser -source 'nuget.org' -force @PassThruParams 173 | } 174 | $GitVersionEXE = ((Get-Package $GitVersionCMDPackageName).source | split-path -Parent) + "\tools\GitVersion.exe" 175 | 176 | #Does this project have a module manifest? Use that as the Gitversion starting point (will use this by default unless project is tagged higher) 177 | #Uses Powershell-YAML module to read/write the GitVersion.yaml config file 178 | if (Test-Path $env:BHPSModuleManifest) { 179 | write-verbose "Fetching Version from Powershell Module Manifest (if present)" 180 | $ModuleManifestVersion = [Version](Get-Metadata $env:BHPSModuleManifest) 181 | if (Test-Path $buildRoot/GitVersion.yml) { 182 | $GitVersionConfigYAML = [ordered]@{} 183 | #ConvertFrom-YAML returns as individual key-value hashtables, we need to combine them into a single hashtable 184 | (Get-Content $GitVersionConfig | ConvertFrom-Yaml) | foreach-object {$GitVersionConfigYAML += $PSItem} 185 | $GitVersionConfigYAML.'next-version' = $ModuleManifestVersion.ToString() 186 | $GitVersionConfigYAML | ConvertTo-Yaml | Out-File $GitVersionConfig 187 | } 188 | else { 189 | @{"next-version" = $ModuleManifestVersion.toString()} | ConvertTo-Yaml | Out-File $GitVersionConfig 190 | } 191 | } 192 | 193 | #Calcuate the GitVersion 194 | write-verbose "Executing GitVersion to determine version info" 195 | $GitVersionCommand = "$GitVersionEXE $buildRoot" 196 | $GitVersionOutput = Invoke-BuildExec { & $GitVersionEXE $buildRoot} 197 | 198 | #Since GitVersion doesn't return error exit codes, we look for error text in the output in the output 199 | if ($GitVersionOutput -match '^[ERROR|INFO] \[') {throw "An error occured when running GitVersion.exe $buildRoot"} 200 | try { 201 | $GitVersionInfo = $GitVersionOutput | ConvertFrom-JSON -ErrorAction stop 202 | } catch { 203 | throw "There was an error when running GitVersion.exe $buildRoot. The output of the command (if any) follows:" 204 | $GitVersionOutput 205 | } 206 | 207 | write-verboseheader "GitVersion Results" 208 | $GitVersionInfo | format-list | out-string | write-verbose 209 | 210 | #If we are in the develop branch, add the prerelease number as revision 211 | #TODO: Make the develop and master regex customizable in a settings file 212 | if ($BranchName -match '^dev(elop)?(ment)?$') { 213 | $SCRIPT:ProjectBuildVersion = ($GitVersionInfo.MajorMinorPatch + "." + $GitVersionInfo.PreReleaseNumber) 214 | } else { 215 | $SCRIPT:ProjectBuildVersion = [Version] $GitVersionInfo.MajorMinorPatch 216 | } 217 | 218 | 219 | $SCRIPT:ProjectSemVersion = $($GitVersionInfo.fullsemver) 220 | write-build Green "Task $($task.name)`: Using Project Version: $ProjectBuildVersion" 221 | write-build Green "Task $($task.name)`: Using Project Version (Extended): $($GitVersionInfo.fullsemver)" 222 | } 223 | 224 | #Copy all powershell module "artifacts" to Build Directory 225 | task CopyFilesToBuildDir { 226 | #Make sure we are in the project location in case somethign changedf 227 | Set-Location $buildRoot 228 | 229 | #The file or file paths to copy, excluding the powershell psm1 and psd1 module and manifest files which will be autodetected 230 | #TODO: Move this somewhere higher in the hierarchy into a settings file, or rather go the "exclude" route 231 | $FilesToCopy = "lib","Public","Private","Types","LICENSE","README.md","$($Env:BHProjectName).psm1","$($Env:BHProjectName).psd1" 232 | copy-item -Recurse -Path $buildRoot\* -Exclude $BuildFilesToExclude -Destination $ProjectBuildPath @PassThruParams 233 | } 234 | 235 | #Update the Metadata of the Module with the latest Version 236 | task UpdateMetadata CopyFilesToBuildDir,Version,{ 237 | # Load the module, read the exported functions, update the psd1 FunctionsToExport 238 | # Because this loads/locks assembiles and can affect cleans in the same session, copy it to a temporary location, find the changes, and apply to original module. 239 | # TODO: Find a cleaner solution, like update Set-ModuleFunctions to use a separate runspace or include a market to know we are in ModuleFunctions so when loading the module we can copy the assemblies to temp files first 240 | $ProjectBuildManifest = ($ProjectBuildPath + "\" + (split-path $env:BHPSModuleManifest -leaf)) 241 | $tempModuleDir = [System.IO.Path]::GetTempFileName() 242 | Remove-Item $tempModuleDir -verbose:$false 243 | New-Item -Type Directory $tempModuleDir | out-null 244 | copy-item -recurse $ProjectBuildPath/* $tempModuleDir 245 | 246 | $TempModuleManifest = ($tempModuleDir + "\" + (split-path $env:BHPSModuleManifest -leaf)) 247 | Set-ModuleFunctions $tempModuleManifest @PassThruParams 248 | $moduleFunctionsToExport = Get-MetaData -Path $tempModuleManifest -PropertyName FunctionsToExport 249 | Update-Metadata -Path $ProjectBuildManifest -PropertyName FunctionsToExport -Value $moduleFunctionsToExport 250 | 251 | # Set the Module Version to the calculated Project Build version 252 | Update-Metadata -Path $ProjectBuildManifest -PropertyName ModuleVersion -Value $ProjectBuildVersion 253 | 254 | # Are we in the master or develop/development branch? Bump the version based on the powershell gallery if so, otherwise add a build tag 255 | if ($BranchName -match '^(master|dev(elop)?(ment)?)$') { 256 | write-build Green "Task $($task.name)`: In Master/Develop branch, adding Tag Version $ProjectBuildVersion to this build" 257 | $Script:ProjectVersion = $ProjectBuildVersion 258 | if (-not (git tag -l $ProjectBuildVersion)) { 259 | git tag "$ProjectBuildVersion" 260 | } else { 261 | write-warning "Tag $ProjectBuildVersion already exists. This is normal if you are running multiple builds on the same commit, otherwise this should not happen" 262 | } 263 | <# TODO: Add some intelligent logic to tagging releases 264 | if (-not $CI) { 265 | git push origin $ProjectBuildVersion | write-verbose 266 | } 267 | #> 268 | <# TODO: Add a Powershell Gallery Check on the module 269 | if (Get-NextNugetPackageVersion -Name (Get-ProjectName) -ErrorAction SilentlyContinue) { 270 | Update-Metadata -Path $env:BHPSModuleManifest -PropertyName ModuleVersion -Value (Get-NextNugetPackageVersion -Name (Get-ProjectName)) 271 | } 272 | #> 273 | } else { 274 | write-build Green "Task $($task.name)`: Not in Master/Develop branch, marking this as a feature prelease build" 275 | $Script:ProjectVersion = $ProjectSemVersion 276 | #Set an email address for tag commit to work if it isn't already present 277 | if (-not (git config user.email)) { 278 | git config user.email "buildtag@$env:ComputerName" 279 | $tempTagGitEmailSet = $true 280 | } 281 | try { 282 | $gitVersionTag = "v$ProjectSemVersion" 283 | if (-not (git tag -l $gitVersionTag)) { 284 | exec { git tag "$gitVersionTag" -a -m "Automatic GitVersion Prerelease Tag Generated by Invoke-Build" } 285 | } else { 286 | write-warning "Tag $gitVersionTag already exists. This is normal if you are running multiple builds on the same commit, otherwise this should not happen" 287 | } 288 | } finally { 289 | if ($tempTagGitEmailSet) { 290 | git config --unset user.email 291 | } 292 | } 293 | 294 | 295 | #Create an empty file in the root directory of the module for easy identification that its not a valid release. 296 | "This is a prerelease build and not meant for deployment!" > (Join-Path $ProjectBuildPath "PRERELEASE-$ProjectSemVersion") 297 | } 298 | 299 | # Add Release Notes from current version 300 | # TODO: Generate Release Notes from Github 301 | #Update-Metadata -Path $env:BHPSModuleManifest -PropertyName ReleaseNotes -Value ("$($env:APPVEYOR_REPO_COMMIT_MESSAGE): $($env:APPVEYOR_REPO_COMMIT_MESSAGE_EXTENDED)") 302 | } 303 | 304 | #Pester Testing 305 | task Pester { 306 | $BuildOutputProject = Join-Path $env:BHBuildOutput $env:BHProjectName 307 | $ModuleManifestPath = Join-Path $BuildOutputProject '\*.psd1' 308 | if (-not (Test-Path $ModuleManifestPath)) {throw "Module Manifest not found at $ModuleManifestPath. Did you run 'Invoke-Build Build' first?"} 309 | 310 | write-verboseheader "Starting Pester Tests..." 311 | $PesterResultFile = "$($env:BHBuildOutput)\$($env:BHProjectName)-TestResults_PS$PSVersion`_$TimeStamp.xml" 312 | 313 | $PesterParams = @{ 314 | Script = "Tests" 315 | OutputFile = $PesterResultFile 316 | OutputFormat = "NunitXML" 317 | PassThru = $true 318 | } 319 | 320 | #If we are in vscode, add the VSCodeMarkers 321 | if ($host.name -match 'Visual Studio Code') { 322 | write-verbose "Detected Visual Studio Code, adding test markers" 323 | $PesterParams.PesterOption = (new-pesteroption -IncludeVSCodeMarker) 324 | } 325 | 326 | Invoke-Pester @PesterParams | Out-Null 327 | 328 | # In Appveyor? Upload our test results! 329 | If ($ENV:APPVEYOR) { 330 | $UploadURL = "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)" 331 | write-verbose "Detected we are running in AppVeyor" 332 | write-verbose "Uploading Pester Results to Appveyor: $UploadURL" 333 | (New-Object 'System.Net.WebClient').UploadFile( 334 | "https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", 335 | $PesterResultFile ) 336 | } 337 | 338 | # Failed tests? 339 | # Need to error out or it will proceed to the deployment. Danger! 340 | if ($TestResults.FailedCount -gt 0) { 341 | Write-Error "Failed '$($TestResults.FailedCount)' tests, build failed" 342 | } 343 | "`n" 344 | } 345 | 346 | task Package Version,{ 347 | 348 | $ZipArchivePath = (join-path $env:BHBuildOutput "$env:BHProjectName-$ProjectBuildVersion.zip") 349 | write-build green "Task $($task.name)`: Writing Finished Module to $ZipArchivePath" 350 | #Package the Powershell Module 351 | Compress-Archive -Path $ProjectBuildPath -DestinationPath $ZipArchivePath -Force @PassThruParams 352 | 353 | $SCRIPT:ArtifactPaths += $ZipArchivePath 354 | #If we are in Appveyor, push completed zip to Appveyor Artifact 355 | if ($env:APPVEYOR) { 356 | write-build Green "Task $($task.name)`: Detected Appveyor, pushing Powershell Module archive to Artifacts" 357 | Push-AppveyorArtifact $ZipArchivePath 358 | } 359 | } 360 | 361 | task PreDeploymentChecks { 362 | #Do not proceed if the most recent Pester test is not passing. 363 | $CurrentErrorActionPreference = $ErrorActionPreference 364 | try { 365 | $ErrorActionPreference = "Stop" 366 | $MostRecentPesterTestResult = [xml]((Get-Content -raw (get-item "$ENV:BHBuildOutput/*-TestResults*.xml" | Sort-Object lastwritetime | Select-Object -last 1))) 367 | $MostRecentPesterTestResult = $MostRecentPesterTestResult."test-results" 368 | if ( 369 | $MostRecentPesterTestResult -isnot [System.XML.XMLElement] -or 370 | $MostRecentPesterTestResult.errors -gt 0 -or 371 | $MostRecentPesterTestResult.failures -gt 0 372 | ) {throw "Fail!"} 373 | } catch { 374 | throw "Unable to detect a clean passing Pester Test nunit xml file in the $env:BHBuildOutput directory. Did you run {Invoke-Build Build,Test} and ensure it passed all tests first?" 375 | } 376 | finally { 377 | $ErrorActionPreference = $CurrentErrorActionPreference 378 | } 379 | 380 | if (($env:BHBranchName -eq 'master') -or $ForceDeploy) { 381 | if (-not (Get-Item $ProjectBuildPath/*.psd1 -erroraction silentlycontinue)) {throw "No Powershell Module Found in $ProjectBuildPath. Skipping deployment. Did you remember to build it first with {Invoke-Build Build}?"} 382 | } else { 383 | write-build Magenta "Task $($task.name)`: We are not in master branch, skipping publish. If you wish to deploy anyways such as for testing, run {InvokeBuild Deploy -ForceDeploy:$true}" 384 | $script:SkipPublish=$true 385 | } 386 | } 387 | 388 | task PublishGitHubRelease -if (-not $SkipPublish) Package,{ 389 | #TODO: Add Prerelease Logic when message commit says "!prerelease" or is in a release branch 390 | if ($AppVeyor -and -not $GitHubAPIKey) { 391 | write-build DarkYellow "Task PublishGitHubRelease: Couldn't find GitHubAPIKey in the Appveyor secure environment variables. Did you save your Github API key as an Appveyor Secure Variable? https://docs.microsoft.com/en-us/powershell/gallery/psgallery/creating-and-publishing-an-item and https://github.com/settings/tokens" 392 | $SkipGitHubRelease = $true 393 | } 394 | if (-not $GitHubAPIKey) { 395 | #TODO: Add Windows Credential Store support and some kind of Linux secure storage or caching option 396 | write-build DarkYellow 'Task PublishGitHubRelease: $env:GitHubAPIKey was not found as an environment variable. Please specify it or use {Invoke-Build Deploy -GitHubUser "MyGitHubUser" -GitHubAPIKey "MyAPIKeyString"}. Have you created a GitHub API key with minimum public_repo scope permissions yet? https://github.com/settings/tokens' 397 | $SkipGitHubRelease = $true 398 | } 399 | if (-not $GitHubUserName) { 400 | write-build DarkYellow 'Task PublishGitHubRelease: $env:GitHubUserName was not found as an environment variable. Please specify it or use {Invoke-Build Deploy -GitHubUser "MyGitHubUser" -GitHubAPIKey "MyAPIKeyString"}. Have you created a GitHub API key with minimum public_repo scope permissions yet? https://github.com/settings/tokens' 401 | $SkipGitHubRelease = $true 402 | } 403 | if ($SkipGitHubRelease) { 404 | write-build Magenta "Task $($task.name): Skipping Publish to GitHub Releases" 405 | } else { 406 | #TODO: Add Prerelease Logic when message commit says "!prerelease" or is in a release branch 407 | #Inspiration from https://www.herebedragons.io/powershell-create-github-release-with-artifact 408 | 409 | #Create the release 410 | $releaseData = @{ 411 | tag_name = [string]::Format("v{0}", $ProjectBuildVersion); 412 | target_commitish = "master"; 413 | name = [string]::Format("v{0}", $ProjectBuildVersion); 414 | body = $env:BHCommitMessage; 415 | draft = $true; 416 | prerelease = $true; 417 | } 418 | $auth = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($GitHubApiKey + ":x-oauth-basic")) 419 | $releaseParams = @{ 420 | Uri = "https://api.github.com/repos/$gitHubUserName/$env:BHProjectName/releases" 421 | Method = 'POST' 422 | Headers = @{ 423 | Authorization = $auth 424 | } 425 | ContentType = 'application/json' 426 | Body = (ConvertTo-Json $releaseData -Compress) 427 | } 428 | 429 | $result = Invoke-RestMethod @releaseParams -ErrorAction stop 430 | 431 | $uploadUriBase = $result.upload_url -creplace '\{\?name,label\}' # Strip the , "?name=$artifact" part 432 | 433 | $uploadParams = @{ 434 | Method = 'POST'; 435 | Headers = @{ 436 | Authorization = $auth; 437 | } 438 | ContentType = 'application/zip'; 439 | } 440 | foreach ($artifactItem in $artifactPaths) { 441 | $uploadparams.URI = $uploadUriBase + "?name=$(split-path $artifactItem -leaf)" 442 | $uploadparams.Infile = $artifactItem 443 | $result = Invoke-RestMethod @uploadParams -erroraction stop 444 | } 445 | } 446 | } 447 | 448 | #TODO: Replace SkipPublish Logic with Proper invokebuild task skipping 449 | task PublishPSGallery -if (-not $SkipPublish) { 450 | if ($AppVeyor -and -not $NuGetAPIKey) { 451 | write-build DarkYellow "Couldn't find NuGetAPIKey in the Appveyor secure environment variables. Did you save your NuGet/Powershell Gallery API key as an Appveyor Secure Variable? https://docs.microsoft.com/en-us/powershell/gallery/psgallery/creating-and-publishing-an-item and https://www.appveyor.com/docs/build-configuration/" 452 | $SkipPSGallery = $true 453 | } 454 | if (-not $NuGetAPIKey) { 455 | #TODO: Add Windows Credential Store support and some kind of Linux secure storage or caching option 456 | write-build DarkYellow '$env:NuGetAPIKey was not found as an environment variable. Please specify it or use {Invoke-Build Deploy -NuGetAPIKey "MyAPIKeyString"}. Have you registered for a Powershell Gallery API key yet? https://docs.microsoft.com/en-us/powershell/gallery/psgallery/creating-and-publishing-an-item' 457 | $SkipPSGallery = $true 458 | } 459 | 460 | if ($SkipPublish) { 461 | Write-Build Magenta "Task $($task.name)`: Skipping Powershell Gallery Publish" 462 | } else { 463 | $publishParams = @{ 464 | Path = $ProjectBuildPath 465 | NuGetApiKey = $NuGetAPIKey 466 | Repository = 'PSGallery' 467 | Force = $true 468 | ErrorAction = 'Stop' 469 | Confirm = $false 470 | } 471 | #TODO: Add Prerelease Logic when message commit says "!prerelease" 472 | Publish-Module @publishParams @PassThruParams 473 | } 474 | } 475 | 476 | ### SuperTasks 477 | # These are the only supported items to run directly from Invoke-Build 478 | task Deploy PreDeploymentChecks,Package,PublishGitHubRelease,PublishPSGallery 479 | task Build Clean, CopyFilesToBuildDir 480 | task Test Pester 481 | 482 | #Default Task - Build, Test with Pester, Deploy 483 | task . Clean,Build,Test,Deploy -------------------------------------------------------------------------------- /PowerHTML.psd1: -------------------------------------------------------------------------------- 1 | # 2 | # Module manifest for module 'PowerHAP' 3 | # 4 | # Generated by: JGrote 5 | # 6 | # Generated on: 3/5/2018 7 | # 8 | 9 | @{ 10 | 11 | # Script module or binary module file associated with this manifest. 12 | RootModule = 'PowerHTML.psm1' 13 | 14 | # Version number of this module. 15 | ModuleVersion = '0.2.0' 16 | 17 | # Supported PSEditions 18 | # CompatiblePSEditions = @() 19 | 20 | # ID used to uniquely identify this module 21 | GUID = '5be91c3d-59a7-469b-bca7-bdc619347b64' 22 | 23 | # Author of this module 24 | Author = 'Justin Grote' 25 | 26 | # Company or vendor of this module 27 | CompanyName = 'Unspecified' 28 | 29 | # Copyright statement for this module 30 | Copyright = '(c) 2024 Justin Grote. All rights reserved.' 31 | 32 | # Description of the functionality provided by this module 33 | Description = 'Provides a wrapper for HTML Agility Pack for use where the IE HTML DOM from Invoke-WebRequest is not available such as Powershell Core' 34 | 35 | # Minimum version of the Windows PowerShell engine required by this module 36 | # PowerShellVersion = '' 37 | 38 | # Name of the Windows PowerShell host required by this module 39 | # PowerShellHostName = '' 40 | 41 | # Minimum version of the Windows PowerShell host required by this module 42 | # PowerShellHostVersion = '' 43 | 44 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 45 | # DotNetFrameworkVersion = '' 46 | 47 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 48 | # CLRVersion = '' 49 | 50 | # Processor architecture (None, X86, Amd64) required by this module 51 | # ProcessorArchitecture = '' 52 | 53 | # Modules that must be imported into the global environment prior to importing this module 54 | # RequiredModules = @() 55 | 56 | # Assemblies that must be loaded prior to importing this module 57 | #RequiredAssemblies = '.\lib\HtmlAgilityPack-1.7.0-netstandard2.dll' 58 | 59 | # Script files (.ps1) that are run in the caller's environment prior to importing this module. 60 | # ScriptsToProcess = @() 61 | 62 | # Type files (.ps1xml) to be loaded when importing this module 63 | # TypesToProcess = @() 64 | 65 | # Format files (.ps1xml) to be loaded when importing this module 66 | FormatsToProcess = @('.\Types\HtmlAgilityPack.HtmlTextNode.ps1xml') 67 | 68 | # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess 69 | # NestedModules = @() 70 | 71 | # 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. 72 | FunctionsToExport = @('ConvertFrom-Html') 73 | 74 | # 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. 75 | #CmdletsToExport = '*' 76 | 77 | # Variables to export from this module 78 | #VariablesToExport = '*' 79 | 80 | # 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. 81 | #AliasesToExport = '*' 82 | 83 | # DSC resources to export from this module 84 | # DscResourcesToExport = @() 85 | 86 | # List of all modules packaged with this module 87 | # ModuleList = @() 88 | 89 | # List of all files packaged with this module 90 | # FileList = @() 91 | 92 | # 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. 93 | PrivateData = @{ 94 | 95 | PSData = @{ 96 | 97 | # Tags applied to this module. These help with module discovery in online galleries. 98 | Tags = @('HTML','Invoke-WebRequest','Scraping','Agility','Pack','ScreenScraping') 99 | 100 | # A URL to the license for this module. 101 | LicenseUri = 'https://github.com/JustinGrote/PowerHTML/blob/master/LICENSE' 102 | 103 | # A URL to the main website for this project. 104 | ProjectUri = 'https://github.com/JustinGrote/PowerHTML' 105 | 106 | # A URL to an icon representing this module. 107 | # IconUri = '' 108 | 109 | # ReleaseNotes of this module 110 | # ReleaseNotes = '' 111 | 112 | } # End of PSData hashtable 113 | 114 | } # End of PrivateData hashtable 115 | 116 | # HelpInfo URI of this module 117 | # HelpInfoURI = '' 118 | 119 | # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. 120 | # DefaultCommandPrefix = '' 121 | 122 | } 123 | -------------------------------------------------------------------------------- /PowerHTML.psm1: -------------------------------------------------------------------------------- 1 | #Get public and private function definition files. 2 | $PublicFunctions = @( Get-ChildItem -Path $PSScriptRoot\Public\*.ps1 -ErrorAction Ignore ) 3 | $PrivateFunctions = @( Get-ChildItem -Path $PSScriptRoot\Private\*.ps1 -ErrorAction Ignore ) 4 | 5 | #Get JSON settings files 6 | $ModuleSettings = @( Get-ChildItem -Path $PSScriptRoot\Settings\*.json -ErrorAction Ignore ) 7 | 8 | #Determine which assembly versions to load 9 | #See if .Net Standard 2.0 is available on the system and if not, load the legacy Net 4.0 library 10 | try { 11 | Add-Type -AssemblyName 'netstandard, Version=2.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51' -ErrorAction Stop 12 | #If netstandard is not available it won't get this far 13 | $dotNetTarget = "netstandard2" 14 | } catch { 15 | $dotNetTarget = "net40-client" 16 | } 17 | 18 | $AssembliesToLoad = Get-ChildItem -Path "$PSScriptRoot\lib\*-$dotNetTarget.dll" 19 | if ($AssembliesToLoad) { 20 | #If we are in a build or a pester test, load assemblies from a temporary file so they don't lock the original file 21 | #This helps to prevent cleaning problems due to a powershell session locking the file because unloading a module doesn't unload assemblies 22 | if ($BuildTask -or $TestDrive) { 23 | write-verbose "Detected Invoke-Build or Pester, loading assemblies from a temp location to avoid locking issues" 24 | if ($Global:BuildAssembliesLoadedPreviously) { 25 | write-warning "You are in a build or test environment. We detected that module assemblies were loaded in this same session on a previous build or test. Strongly recommend you kill the process and start a new session for a clean build/test!" 26 | } 27 | 28 | $TempAssembliesToLoad = @() 29 | foreach ($AssemblyPathItem in $AssembliesToLoad) { 30 | $TempAssemblyPath = [System.IO.Path]::GetTempFileName() + ".dll" 31 | Copy-Item $AssemblyPathItem $TempAssemblyPath 32 | $TempAssembliesToLoad += [System.IO.FileInfo]$TempAssemblyPath 33 | } 34 | $AssembliesToLoad = $TempAssembliesToLoad 35 | $Global:BuildAssembliesLoadedPreviously = $true 36 | } 37 | 38 | write-verbose "Loading Assemblies for .NET target: $dotNetTarget" 39 | Add-Type -Path $AssembliesToLoad.fullname -ErrorAction Stop 40 | } 41 | 42 | #Dot source the files 43 | foreach ($FunctionToImport in @($PublicFunctions + $PrivateFunctions)) { 44 | try { 45 | . $FunctionToImport.fullname 46 | } catch { 47 | Write-Error -Message "Failed to import function $($import.fullname): $_" 48 | } 49 | } 50 | 51 | #Import Settings files as global objects based on their filename 52 | foreach ($ModuleSettingsItem in $ModuleSettings) { 53 | New-Variable -Name "$($ModuleSettingsItem.basename)" -Scope Global -Value (convertfrom-json (Get-Content -raw $ModuleSettingsItem.fullname)) -Force 54 | } -------------------------------------------------------------------------------- /Private/.placeholder: -------------------------------------------------------------------------------- 1 | placeholder -------------------------------------------------------------------------------- /Public/ConvertFrom-HTML.ps1: -------------------------------------------------------------------------------- 1 |  2 | function ConvertFrom-Html { 3 | <# 4 | .SYNOPSIS 5 | Takes an HTML input and converts it to an HTMLAgilityPack htmlNode object that can be navigated using Linq 6 | .DESCRIPTION 7 | Long description 8 | .EXAMPLE 9 | $HTMLString = @' 10 | 11 | 12 | 13 |

My First Heading

14 |

My first paragraph.

d 15 | 16 | 17 | '@ | ConvertFrom-HTML 18 | 19 | $HTMLString 20 | 21 | NodeType Name AttributeCount ChildNodeCount ContentLength InnerText 22 | -------- ---- -------------- -------------- ------------- --------- 23 | Document #document 0 4 103 … 24 | 25 | $HTMLString.SelectSingleNode('//body/h1') 26 | 27 | NodeType Name AttributeCount ChildNodeCount ContentLength InnerText 28 | -------- ---- -------------- -------------- ------------- --------- 29 | Element h1 0 1 16 My First Heading 30 | 31 | Convert HTML string to a HtmlNode via the pipeline. 32 | 33 | .EXAMPLE 34 | $uri = [Uri]'https://www.powershellgallery.com/' | ConvertFrom-HTML 35 | $uri 36 | 37 | NodeType Name AttributeCount ChildNodeCount ContentLength InnerText 38 | -------- ---- -------------- -------------- ------------- --------- 39 | Document #document 0 4 17550 … 40 | 41 | Fetch and parse a url. 42 | .EXAMPLE 43 | Get-Item $testFilePath | ConvertFrom-Html 44 | 45 | NodeType Name AttributeCount ChildNodeCount ContentLength InnerText 46 | -------- ---- -------------- -------------- ------------- --------- 47 | Document #document 0 5 105 … 48 | 49 | Parse an HTML file piped from Get-Item. 50 | .INPUTS 51 | [String[]] 52 | [System.IO.FileInfo[]] 53 | [System.URI[]] 54 | .OUTPUTS 55 | [HtmlAgilityPack.HtmlDocument] 56 | [HtmlAgilityPack.HtmlNode] 57 | .NOTES 58 | General notes 59 | #> 60 | [OutputType([HtmlAgilityPack.HtmlNode])] 61 | [OutputType([HtmlAgilityPack.HtmlDocument])] 62 | [CmdletBinding(DefaultParameterSetName = 'String')] 63 | param( 64 | #The HTML text to parse. Accepts multiple separate documents as an array. This also accepts pipeline from Invoke-WebRequest 65 | [Parameter(ParameterSetName = 'String', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] 66 | [String[]] $Content, 67 | 68 | #The URI or URIs from which to retrieve content. This may be faster than using Invoke-WebRequest but is less flexible in the method of retrieval (for instance, no POST) 69 | [Parameter(ParameterSetName = 'URI', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] 70 | [System.URI[]] $URI, 71 | 72 | #Path to file or files containing HTML content to convert. This accepts pipeline from Get-Childitem or Get-Item 73 | [Parameter(ParameterSetName = 'Path', Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)] 74 | [System.IO.FileInfo[]] $Path, 75 | 76 | #Do not return the Linq documentnode, instead return the HTMLDocument object. This is useful if you want to do XPath queries instead of Linq queries 77 | [switch] $Raw 78 | ) 79 | begin { 80 | $html = [HtmlAgilityPack.HtmlDocument]::new() 81 | $web = [HtmlAgilityPack.HtmlWeb]::new() 82 | } 83 | process { 84 | switch ($PSCmdlet.ParameterSetName) { 85 | 'String' { 86 | $Content | ForEach-Object { 87 | Write-Verbose "Loading HTML" 88 | $html.LoadHtml($_) 89 | if ($Raw) { $html } else { $html.DocumentNode } 90 | } 91 | } 92 | 'URI' { 93 | $URI | ForEach-Object { 94 | Write-Verbose "Loading URI $_" 95 | $site = $web.Load($_) 96 | if ($Raw) { $site } else { $site.DocumentNode } 97 | } 98 | } 99 | 'Path' { 100 | $Path | ForEach-Object { 101 | Write-Verbose "Loading File $_" 102 | $html.Load($_.FullName) 103 | if ($Raw) { $html } else { $html.DocumentNode } 104 | } 105 | } 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PowerHTML 2 | [![Build status](https://ci.appveyor.com/api/projects/status/qw79sc8cu0u0y8b8/branch/master?svg=true)](https://ci.appveyor.com/project/JustinGrote/powerhtml/branch/master) 3 | 4 | HTML Agility Pack implementation in Powershell for parsing and manipulating HTML 5 | 6 | Initially this project provides the ConvertFrom-HTML cmdlet, which can be used to parse HTML without requiring IE and HTML document. 7 | -------------------------------------------------------------------------------- /Tests/00-PowershellModule.Tests.ps1: -------------------------------------------------------------------------------- 1 | #requires -module BuildHelpers 2 | 3 | #Must be done during discovery as contexts contain build info 4 | Set-BuildEnvironment -Force 5 | 6 | BeforeAll { 7 | if (-not (Import-Module BuildHelpers -PassThru -Verbose:$false -ErrorAction silentlycontinue)) { 8 | Install-Module BuildHelpers -Scope currentuser -ErrorAction stop -Force 9 | Import-Module BuildHelpers -ErrorAction stop -Verbose:$false 10 | } 11 | $SCRIPT:PSVersion = $PSVersionTable.PSVersion.Major 12 | $SCRIPT:BuildOutputProject = Join-Path $env:BHBuildOutput $env:BHProjectName 13 | $SCRIPT:ModuleManifestPath = Join-Path $BuildOutputProject '\*.psd1' 14 | if (-not (Test-Path $ModuleManifestPath)) { throw "Module Manifest not found at $ModuleManifestPath. Did you run 'Invoke-Build Build' first?" } 15 | } 16 | 17 | Describe 'Powershell Module' { 18 | Context "$env:BHProjectName" { 19 | BeforeAll { 20 | $SCRIPT:ModuleName = $env:BHProjectName 21 | } 22 | It 'Has a valid Module Manifest' { 23 | if ($isCoreCLR -or $PSVersionTable.PSVersion -ge [Version]'5.1') { 24 | $Script:Manifest = Test-ModuleManifest $ModuleManifestPath 25 | } else { 26 | #Copy the Module Manifest to a temp file in order to test to fix a bug where 27 | #Test-ModuleManifest caches the first result, thus not catching changes 28 | $TempModuleManifestPath = [System.IO.Path]::GetTempFileName() + '.psd1' 29 | Copy-Item $ModuleManifestPath $TempModuleManifestPath 30 | $Script:Manifest = Test-ModuleManifest $TempModuleManifestPath 31 | Remove-Item $TempModuleManifestPath -Verbose:$false 32 | } 33 | } 34 | 35 | It 'Has a valid root module' { 36 | $Manifest.RootModule | Should -Be "$ModuleName.psm1" 37 | } 38 | 39 | It 'Has a valid Description' { 40 | $Manifest.Description | Should -Not -BeNullOrEmpty 41 | } 42 | 43 | It 'Has a valid GUID' { 44 | [Guid]$Manifest.Guid | Should -BeOfType System.GUID 45 | } 46 | 47 | It 'Has a valid Copyright' { 48 | $Manifest.Copyright | Should -Not -BeNullOrEmpty 49 | } 50 | 51 | It 'Exports all public functions' { 52 | $FunctionFiles = Get-ChildItem "$BuildOutputProject\Public" -Filter *.ps1 | Select-Object -ExpandProperty BaseName 53 | $FunctionNames = $FunctionFiles | ForEach-Object { $_ -replace '-', "-$($Manifest.Prefix)" } 54 | $ExFunctions = $Manifest.ExportedFunctions.Values.Name 55 | foreach ($FunctionName in $FunctionNames) { 56 | $ExFunctions -contains $FunctionName | Should -BeTrue 57 | } 58 | } 59 | 60 | It 'Has at least 1 exported command' { 61 | $Script:Manifest.exportedcommands.count | Should -BeGreaterThan 0 62 | } 63 | It 'Can be imported as a module successfully' { 64 | Remove-Module $ModuleName -ErrorAction SilentlyContinue 65 | Import-Module $BuildOutputProject -PassThru -Verbose:$false -OutVariable BuildOutputModule | Should -BeOfType System.Management.Automation.PSModuleInfo 66 | $BuildOutputModule.Name | Should -Be $ModuleName 67 | } 68 | It 'Is visible in Get-Module' { 69 | $module = Get-Module $ModuleName 70 | $Module | Should -BeOfType System.Management.Automation.PSModuleInfo 71 | $Module.Name | Should -Be $ModuleName 72 | } 73 | } 74 | } 75 | 76 | Describe 'PSScriptAnalyzer' { 77 | BeforeAll { 78 | $SCRIPT:SAResults = Invoke-ScriptAnalyzer -Path $BuildOutputProject -Recurse -ExcludeRule 'PSAvoidUsingCmdletAliases', 'PSAvoidGlobalVars' -Verbose:$false 79 | } 80 | 81 | It 'PSScriptAnalyzer returns zero errors for all files in the repository' { 82 | $SAResults.Count | Should -Be 0 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Tests/PowerHTML.Tests.ps1: -------------------------------------------------------------------------------- 1 | #Move out of tests to the subdirectory of the modulepwd 2 | if ((get-item .).Name -match 'Tests') { Set-Location $PSScriptRoot\.. } 3 | 4 | Describe 'HTML Basic Conversion' { 5 | BeforeAll { 6 | if (-not (Get-Module PowerHTML)) { 7 | Import-Module $PSScriptRoot\..\PowerHTML.psd1 8 | } 9 | $HTMLString = @' 10 | 11 | 12 | 13 |

My First Heading

14 |

My first paragraph.

15 | 16 | 17 | '@ 18 | $HTMLString2 = @' 19 | 20 | 21 | 22 |

Heading 1

23 |

Paragraph 1.

24 | 25 | 26 | '@ 27 | #Generate test files to a random path 28 | $testFilePath1 = New-TemporaryFile 29 | $testFilePath2 = New-TemporaryFile 30 | $testFilePathAll = @($testFilePath1,$testFilePath2) 31 | Set-Content -Path $testFilePath1 -Value $HTMLString 32 | Set-Content -Path $testFilePath2 -Value $HTMLString2 33 | } 34 | It 'Can convert an HTML string to a raw HTMLDocument via the pipeline' { 35 | $HTMLString | ConvertFrom-Html -Raw | Should -BeOfType HtmlAgilityPack.HTMLDocument 36 | } 37 | It 'Can parse an HTML string to a HtmlNode via the pipeline' { 38 | $HTMLString | ConvertFrom-Html | Should -BeOfType HtmlAgilityPack.HTMLNode 39 | } 40 | It 'Can parse multiple HTML strings to HtmlNodes when passed via the pipeline as an array' { 41 | $result = $HTMLString,$HTMLString2 | ConvertFrom-HTML 42 | $result.count | Should -Be 2 43 | foreach ($resultItem in $result) { 44 | $resultItem | Should -BeOfType HtmlAgilityPack.HTMLNode 45 | } 46 | } 47 | It 'Can parse an HTML file' { 48 | ConvertFrom-Html -Path $testFilePath1 | Should -BeOfType HtmlAgilityPack.HTMLNode 49 | } 50 | It 'Can parse multiple HTML files' { 51 | $result = ConvertFrom-Html -Path $testFilePath1,$testFilePath2 52 | $result.count | Should -Be 2 53 | foreach ($resultItem in $result) { 54 | $resultItem | Should -BeOfType HtmlAgilityPack.HTMLNode 55 | } 56 | } 57 | It 'Can parse an HTML file piped from Get-Item' { 58 | Get-Item $testFilePath1 | ConvertFrom-Html | Should -BeOfType HtmlAgilityPack.HTMLNode 59 | } 60 | It 'Can parse multiple HTML files piped from Get-Item' { 61 | $result = Get-Item $testFilePathAll | ConvertFrom-Html 62 | $result.count | Should -Be 2 63 | foreach ($resultItem in $result) { 64 | $resultItem | Should -BeOfType HtmlAgilityPack.HTMLNode 65 | } 66 | } 67 | AfterAll { 68 | Remove-Item $testFilePath1,$testFilePath2 -ErrorAction silentlycontinue -force 69 | } 70 | 71 | } 72 | 73 | Describe 'HTTP Operational Tests - REQUIRES INTERNET CONNECTION!' { 74 | BeforeAll { 75 | $uri = 'https://www.google.com' 76 | $uriObjects = [uri]$uri, [uri]'https://www.facebook.com', [uri]'https://www.x.com' 77 | } 78 | It 'Can fetch and parse $uri directly via the URI pipeline' { 79 | $result = ConvertFrom-Html -URI $uri 80 | $result | Should -BeOfType HtmlAgilityPack.HTMLNode 81 | $result.innertext -match 'Google' | Should -BeTrue 82 | } 83 | It 'Can parse $uri piped from Invoke-WebRequest' { 84 | $result = Invoke-WebRequest -Verbose:$false $uri | ConvertFrom-Html 85 | $result | Should -BeOfType HtmlAgilityPack.HTMLNode 86 | $result.innertext -match 'Google' | Should -BeTrue 87 | } 88 | It 'Can parse multiple URI objects passed via the pipeline (Google,Facebook,Twiiter)' { 89 | $result = $uriObjects | ConvertFrom-Html 90 | foreach ($resultItem in $result) { 91 | $resultItem | Should -BeOfType HtmlAgilityPack.HTMLNode 92 | } 93 | $result[0].innertext | Should -Match 'Google' 94 | $result[1].innertext | Should -Match 'Facebook' 95 | $result[2].innertext | Should -Match 'X\.com' 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /Types/HtmlAgilityPack.HtmlTextNode.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | HtmlAgilityPack.HtmlNode 6 | 7 | HtmlAgilityPack.HtmlNode 8 | HtmlAgilityPack.HtmlTextNode 9 | HtmlAgilityPack.HtmlCommentNode 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | NodeType 31 | 32 | 33 | Name 34 | 35 | 36 | $PSItem.Attributes.Count 37 | 38 | 39 | $PSItem.ChildNodes.Count 40 | 41 | 42 | $PSItem.InnerHTML.Length 43 | 44 | 45 | InnerText 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /lib/HtmlAgilityPack-1.11.60-net40-client.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinGrote/PowerHTML/a74a655cee4c979e564c945d5b4aa076e62e887f/lib/HtmlAgilityPack-1.11.60-net40-client.dll -------------------------------------------------------------------------------- /lib/HtmlAgilityPack-1.11.60-netstandard2.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JustinGrote/PowerHTML/a74a655cee4c979e564c945d5b4aa076e62e887f/lib/HtmlAgilityPack-1.11.60-netstandard2.dll -------------------------------------------------------------------------------- /lib/HtmlAgilityPack-1.11.60.LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Permission is hereby granted, free of charge, to any person obtaining a copy 3 | of this software and associated documentation files (the "Software"), to deal 4 | in the Software without restriction, including without limitation the rights 5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 6 | copies of the Software, and to permit persons to whom the Software is 7 | furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all 10 | copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 18 | SOFTWARE. --------------------------------------------------------------------------------