diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..36bd853 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [StartAutomating] diff --git a/.github/workflows/BuildMathML.yml b/.github/workflows/BuildMathML.yml new file mode 100644 index 0000000..fb13639 --- /dev/null +++ b/.github/workflows/BuildMathML.yml @@ -0,0 +1,501 @@ + +name: Build MathML Module +on: + push: + pull_request: + workflow_dispatch: +jobs: + TestPowerShellOnLinux: + runs-on: ubuntu-latest + steps: + - name: InstallPester + id: InstallPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Installs Pester + .Description + Installs Pester + #> + param( + # The maximum pester version. Defaults to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99' + ) + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber + Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters + - name: Check out repository + uses: actions/checkout@v2 + - name: RunPester + id: RunPester + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.PesterMaxVersion = ${env:PesterMaxVersion} + $Parameters.NoCoverage = ${env:NoCoverage} + $Parameters.NoCoverage = $parameters.NoCoverage -match 'true'; + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {<# + .Synopsis + Runs Pester + .Description + Runs Pester tests after importing a PowerShell module + #> + param( + # The module path. If not provided, will default to the second half of the repository ID. + [string] + $ModulePath, + # The Pester max version. By default, this is pinned to 4.99.99. + [string] + $PesterMaxVersion = '4.99.99', + + # If set, will not collect code coverage. + [switch] + $NoCoverage + ) + + $global:ErrorActionPreference = 'continue' + $global:ProgressPreference = 'silentlycontinue' + + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" } + $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion + $importedModule = Import-Module $ModulePath -Force -PassThru + $importedPester, $importedModule | Out-Host + + $codeCoverageParameters = @{ + CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1" + CodeCoverageOutputFile = ".\$moduleName.Coverage.xml" + } + + if ($NoCoverage) { + $codeCoverageParameters = @{} + } + + + $result = + Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters + + "::set-output name=TotalCount::$($result.TotalCount)", + "::set-output name=PassedCount::$($result.PassedCount)", + "::set-output name=FailedCount::$($result.FailedCount)" | Out-Host + if ($result.FailedCount -gt 0) { + "::debug:: $($result.FailedCount) tests failed" + foreach ($r in $result.TestResult) { + if (-not $r.Passed) { + "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)" + } + } + throw "::error:: $($result.FailedCount) tests failed" + } + } @Parameters + - name: PublishTestResults + uses: actions/upload-artifact@main + with: + name: PesterResults + path: '**.TestResults.xml' + if: ${{always()}} + TagReleaseAndPublish: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@v2 + - name: TagModuleVersion + id: TagModuleVersion + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat} + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The tag version format (default value: '$($imported.Name) $(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagAnnotationFormat = '$($imported.Name) $($imported.Version)' + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Tagging" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $existingTags = git tag --list + + @" + Target Version: $targetVersion + + Existing Tags: + $($existingTags -join [Environment]::NewLine) + "@ | Out-Host + + $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion } + + if ($versionTagExists) { + "::warning::Version $($versionTagExists)" + return + } + + if (-not $UserName) { $UserName = $env:GITHUB_ACTOR } + if (-not $UserEmail) { $UserEmail = "$UserName@github.com" } + git config --global user.email $UserEmail + git config --global user.name $UserName + + git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat) + git push origin --tags + + if ($env:GITHUB_ACTOR) { + exit 0 + }} @Parameters + - name: ReleaseModule + id: ReleaseModule + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.UserEmail = ${env:UserEmail} + $Parameters.UserName = ${env:UserName} + $Parameters.TagVersionFormat = ${env:TagVersionFormat} + $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat} + $Parameters.ReleaseAsset = ${env:ReleaseAsset} + $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + # The user email associated with a git commit. + [string] + $UserEmail, + + # The user name associated with a git commit. + [string] + $UserName, + + # The tag version format (default value: 'v$(imported.Version)') + # This can expand variables. $imported will contain the imported module. + [string] + $TagVersionFormat = 'v$($imported.Version)', + + # The release name format (default value: '$($imported.Name) $($imported.Version)') + [string] + $ReleaseNameFormat = '$($imported.Name) $($imported.Version)', + + # Any assets to attach to the release. Can be a wildcard or file name. + [string[]] + $ReleaseAsset + ) + + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host + return + } + + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat) + $targetReleaseName = $targetVersion + $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases' + "Release URL: $releasesURL" | Out-Host + $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + + $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion + + if ($releaseExists) { + "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host + $releasedIt = $releaseExists + } else { + $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body ( + [Ordered]@{ + owner = '${{github.owner}}' + repo = '${{github.repository}}' + tag_name = $targetVersion + name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat) + body = + if ($env:RELEASENOTES) { + $env:RELEASENOTES + } elseif ($imported.PrivateData.PSData.ReleaseNotes) { + $imported.PrivateData.PSData.ReleaseNotes + } else { + "$($imported.Name) $targetVersion" + } + draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false } + prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false } + } | ConvertTo-Json + ) -Headers @{ + "Accept" = "application/vnd.github.v3+json" + "Content-type" = "application/json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } + } + + + + + + if (-not $releasedIt) { + throw "Release failed" + } else { + $releasedIt | Out-Host + } + + $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$' + + if ($ReleaseAsset) { + $fileList = Get-ChildItem -Recurse + $filesToRelease = + @(:nextFile foreach ($file in $fileList) { + foreach ($relAsset in $ReleaseAsset) { + if ($relAsset -match '[\*\?]') { + if ($file.Name -like $relAsset) { + $file; continue nextFile + } + } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) { + $file; continue nextFile + } + } + }) + + $releasedFiles = @{} + foreach ($file in $filesToRelease) { + if ($releasedFiles[$file.Name]) { + Write-Warning "Already attached file $($file.Name)" + continue + } else { + $fileBytes = [IO.File]::ReadAllBytes($file.FullName) + $releasedFiles[$file.Name] = + Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{ + "Accept" = "application/vnd.github+json" + "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}' + } -Body $fileBytes -ContentType Application/octet-stream + $releasedFiles[$file.Name] + } + } + + "Attached $($releasedFiles.Count) file(s) to release" | Out-Host + } + + + + } @Parameters + - name: PublishPowerShellGallery + id: PublishPowerShellGallery + shell: pwsh + run: | + $Parameters = @{} + $Parameters.ModulePath = ${env:ModulePath} + $Parameters.Exclude = ${env:Exclude} + $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$' + foreach ($k in @($parameters.Keys)) { + if ([String]::IsNullOrEmpty($parameters[$k])) { + $parameters.Remove($k) + } + } + Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')" + & {param( + [string] + $ModulePath, + + [string[]] + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*') + ) + + $gitHubEvent = if ($env:GITHUB_EVENT_PATH) { + [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json + } else { $null } + + if (-not $Exclude) { + $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*') + } + + + @" + ::group::GitHubEvent + $($gitHubEvent | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + @" + ::group::PSBoundParameters + $($PSBoundParameters | ConvertTo-Json -Depth 100) + ::endgroup:: + "@ | Out-Host + + if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and + (-not $gitHubEvent.psobject.properties['inputs'])) { + "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host + return + } + + + $imported = + if (-not $ModulePath) { + $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/" + Import-Module ".\$moduleName.psd1" -Force -PassThru -Global + } else { + Import-Module $modulePath -Force -PassThru -Global + } + + if (-not $imported) { return } + + $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {} + + if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) { + "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host + } else { + + $gk = '${{secrets.GALLERYKEY}}' + + $rn = Get-Random + $moduleTempFolder = Join-Path $pwd "$rn" + $moduleTempPath = Join-Path $moduleTempFolder $moduleName + New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host + + Write-Host "Staging Directory: $ModuleTempPath" + + $imported | Split-Path | + Get-ChildItem -Force | + Where-Object Name -NE $rn | + Copy-Item -Destination $moduleTempPath -Recurse + + $moduleGitPath = Join-Path $moduleTempPath '.git' + Write-Host "Removing .git directory" + if (Test-Path $moduleGitPath) { + Remove-Item -Recurse -Force $moduleGitPath + } + + if ($Exclude) { + "::notice::Attempting to Exlcude $exclude" | Out-Host + Get-ChildItem $moduleTempPath -Recurse | + Where-Object { + foreach ($ex in $exclude) { + if ($_.FullName -like $ex) { + "::notice::Excluding $($_.FullName)" | Out-Host + return $true + } + } + } | + Remove-Item + } + + Write-Host "Module Files:" + Get-ChildItem $moduleTempPath -Recurse + Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery" + Publish-Module -Path $moduleTempPath -NuGetApiKey $gk + if ($?) { + Write-Host "Published to Gallery" + } else { + Write-Host "Gallery Publish Failed" + exit 1 + } + } + } @Parameters + BuildMathML: + runs-on: ubuntu-latest + if: ${{ success() }} + steps: + - name: Check out repository + uses: actions/checkout@main + - name: UseEZOut + uses: StartAutomating/EZOut@master +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} diff --git a/Build/GitHub/Jobs/BuildMathML.psd1 b/Build/GitHub/Jobs/BuildMathML.psd1 new file mode 100644 index 0000000..e27509e --- /dev/null +++ b/Build/GitHub/Jobs/BuildMathML.psd1 @@ -0,0 +1,18 @@ +@{ + "runs-on" = "ubuntu-latest" + if = '${{ success() }}' + steps = @( + @{ + name = 'Check out repository' + uses = 'actions/checkout@main' + }, + 'RunEZOut' # , + <#@{ + name = 'Run MathML (on branch)' + if = '${{github.ref_name != ''main''}}' + uses = './' + id = 'MathMLAction' + }#> + # 'BuildAndPublishContainer' + ) +} \ No newline at end of file diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1 new file mode 100644 index 0000000..e8111e8 --- /dev/null +++ b/Build/GitHub/Steps/PublishTestResults.psd1 @@ -0,0 +1,10 @@ +@{ + name = 'PublishTestResults' + uses = 'actions/upload-artifact@main' + with = @{ + name = 'PesterResults' + path = '**.TestResults.xml' + } + if = '${{always()}}' +} + diff --git a/Build/MathML.GitHubWorkflow.PSDevOps.ps1 b/Build/MathML.GitHubWorkflow.PSDevOps.ps1 new file mode 100644 index 0000000..be2d021 --- /dev/null +++ b/Build/MathML.GitHubWorkflow.PSDevOps.ps1 @@ -0,0 +1,15 @@ +#requires -Module PSDevOps +Import-BuildStep -SourcePath ( + Join-Path $PSScriptRoot 'GitHub' +) -BuildSystem GitHubWorkflow + +Push-Location ($PSScriptRoot | Split-Path) +New-GitHubWorkflow -Name "Build MathML Module" -On Push, + PullRequest, + Demand -Job TestPowerShellOnLinux, + TagReleaseAndPublish, BuildMathML -Environment ([Ordered]@{ + REGISTRY = 'ghcr.io' + IMAGE_NAME = '${{ github.repository }}' + }) -OutputPath .\.github\workflows\BuildMathML.yml + +Pop-Location \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a2a27bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +## MathML 0.1 + +* Initial release of MathML (#1) +* Core Commands: + * Get-MathML (#2) + * Export-MathML (#3) + * Import-MathML (#4) +* Extended Types + * `MathML.get_SVG` (#5) + * `MathML.ToString()` (#8) + * `MathML.get/set_ID` (#9) +* MathML schemas included (#7) +* Initial Workflow (#6) diff --git a/Commands/Export-MathML.ps1 b/Commands/Export-MathML.ps1 new file mode 100644 index 0000000..cea6dbd --- /dev/null +++ b/Commands/Export-MathML.ps1 @@ -0,0 +1,102 @@ +function Export-MathML { + <# + .SYNOPSIS + Exports MathML + .DESCRIPTION + Exports MathML into a file + .EXAMPLE + MathML https://dlmf.nist.gov/2.1 | + Export-MathML ./dlmf.2.1.html + #> + [Alias('Save-MathML')] + param( + # The export file path. + [Parameter(Mandatory)] + [Alias('Fullname')] + [string] + $FilePath, + + # Any input objects. + [Parameter(ValueFromPipeline)] + [PSObject[]] + $InputObject, + + # If set, will force an export, even if a file already exists. + [switch] + $Force + ) + + # Gather all the input + $allInput = @($input) + + # If nothing was passed + if ($allInput.Length -eq 0) { + # briefly check for non-piped -InputObject + if ($PSBoundParameters.InputObject) { + $allInput = @($PSBoundParameters.InputObject | . { process { $_ } }) + } + # If we still have no input, return (there is nothing to export) + if ($allInput.Length -eq 0) {return} + } + + # Find the full path, but do not resolve it + $unresolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($FilePath) + # If it already existed, and we are not using the `-Force` + if ((Test-Path $unresolvedPath) -and -not $Force) { + # write an error + Write-Error "$unresolvedPath already exists, use -Force" + # and return + return + } + + + # IF we have one MathML + if ($allInput.Length -eq 1 -and $allInput[0] -is [xml]) { + # save that to a file + $newFile = New-Item -Path $unresolvedPath -Force -ItemType File + # If the extension was .svg or .html, and the input has an SVG + if ($newFile.Extension -in '.svg', '.html' -and $allInput[0].SVG -is [xml]) { + # save the SVG to the file + $allInput[0].SVG.Save($newFile.FullName) + } else { + # otherwise, save the XML to the file + $allInput[0].Save($newFile.FullName) + } + } + # If we have multiple MathML + else { + # we can store them in an XHTML file + $html = @( + # construct a simple header + "MathML" + foreach ($in in $allInput) { + # and put each MathML within a div + "
" + + # If it was XML + if ($in -is [xml]) { + $in.OuterXml # put it inline + } + # If there was a SVG property + elseif ($in.SVG) { + # put that inline + $in.SVG.OuterXml + } + # If there was a HTML property + elseif ($in.HTML) { + # put that inline (if it was unbalanced, export will not work) + $in.HTML + } + # last but not least, escape any text + else { + [Security.SecurityElement]::Escape("$in") + } + "
" + } + "" + ) -join [Environment]::NewLine + + # Create a new file containing the HTML + New-Item -Path $unresolvedPath -Force -ItemType File -Value $html + } +} diff --git a/Commands/Get-MathML.ps1 b/Commands/Get-MathML.ps1 new file mode 100644 index 0000000..3a0e3ab --- /dev/null +++ b/Commands/Get-MathML.ps1 @@ -0,0 +1,140 @@ +function Get-MathML +{ + <# + .SYNOPSIS + Gets MathML + .DESCRIPTION + Gets MathML from a file or page + .EXAMPLE + MathML https://dlmf.nist.gov/2.1 + .EXAMPLE + MathML 'https://en.wikipedia.org/wiki/Rose_(mathematics)' + .EXAMPLE + MathML " + + + 1 + + + 1 + = + 2 + + + " + #> + [Alias('MathML')] + param( + # A url or file path that hopefully contains MathML + # The response from this URL will be cached. + [Parameter(ValueFromPipelineByPropertyName)] + [Alias('Uri','FilePath','Fullname')] + [string] + $Url, + + # If set, will request the URL, even if it has been cached. + [Parameter(ValueFromPipelineByPropertyName)] + [switch] + $Force, + + # If set, will use chromium to request the page, and will + [Parameter(ValueFromPipelineByPropertyName)] + [switch] + $UseChromium, + + # The path to a chromium browser. + [Parameter(ValueFromPipelineByPropertyName)] + [string] + $ChromiumPath = 'chromium' + ) + + begin { + if (-not $script:MathMLCache) { + $script:MathMLCache = [Ordered]@{} + } + + $mathMlPattern = [Regex]::new('','IgnoreCase') + } + + process { + # If we have no URL + if (-not $PSBoundParameters.Url) { + # get any loaded MathML + $mathMLValues = @($script:MathMLCache.Values.MathML) + if ($mathMLValues) { + # unroll each result + foreach ($value in $mathMLValues) { + if (-not $value) { continue } + # and return non-null values + $value + } + } + return + } + + # If we have not yet cached this URL, or we are using the `-Force` + if (-not $script:MathMLCache["$url"] -or $Force) { + # Create a cache object + $script:MathMLCache["$url"] = [Ordered]@{ + Response = + # If the URL could be XML + if ($url -as [xml]) { + # use that as the source. + ($url -as [xml]).OuterXml + } + # If the URL was actually a file path + elseif (Test-Path $url) + { + # get it's content. + Get-Content -Raw $Url + } + # If we are not using chromium, + elseif (-not $UseChromium) + { + # use Invoke-RestMethod to get the URL + Invoke-RestMethod $url + } + # If we are using chromium + else + { + # Call chromium in headless mode and dump DOM + & $ChromiumPath --headless --disable-gpu --no-sandbox --dump-dom "$url" *>&1 | + # strip out any chromium trace messages + Where-Object { $_ -notmatch '^\[\d+:\d+' } | + # and stringify the whole response. + Out-String -Width 1mb + } + } + } + + # If we have a response for this URL, but no MathML yet + if ( + $script:MathMLCache["$url"].Response -and -not + $script:MathMLCache["$url"].MathML + ) { + $script:MathMLCache["$url"].MathML = + # find any matches for our pattern + @(foreach ($match in $mathMlPattern.Matches("$( + $script:MathMLCache["$url"].Response + )")) { + # and cast them into XML. + $matchXml = $match.Value -as [xml] + + if (-not $matchXML) { continue } + # If they do not have the xml namespace + if (-not $matchXML.math.xmlns) { + # add it + $matchXML.math.setAttribute('xmlns', 'http://www.w3.org/1998/Math/MathML') + } + # decorate the return as MathML + $matchXml.pstypenames.insert(0, 'MathML') + # and output it to the cache + $matchXml + }) + + } + + # Last but not least, output any MathML objects in the cache for this URL. + $script:MathMLCache["$url"].MathML + } +} + diff --git a/Commands/Import-MathML.ps1 b/Commands/Import-MathML.ps1 new file mode 100644 index 0000000..0a69383 --- /dev/null +++ b/Commands/Import-MathML.ps1 @@ -0,0 +1,24 @@ +function Import-MathML +{ + <# + .SYNOPSIS + Imports MathML + .DESCRIPTION + Imports MathML from a file or URL + .LINK + Get-MathML + #> + [Alias('Restore-MathML')] + param( + # The path to a file or URL that hopefully contains MathML + [Parameter(Mandatory,ValueFromPipelineByPropertyName)] + [string] + $FilePath + ) + + process { + # This is an extremely light wrapper of Get-MathML. + Get-MathML @PSBoundParameters + } + +} diff --git a/Data/mathml3-common.xsd b/Data/mathml3-common.xsd new file mode 100755 index 0000000..98b801d --- /dev/null +++ b/Data/mathml3-common.xsd @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Data/mathml3-content.xsd b/Data/mathml3-content.xsd new file mode 100755 index 0000000..160c25b --- /dev/null +++ b/Data/mathml3-content.xsd @@ -0,0 +1,684 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Data/mathml3-presentation.xsd b/Data/mathml3-presentation.xsd new file mode 100755 index 0000000..418cbab --- /dev/null +++ b/Data/mathml3-presentation.xsd @@ -0,0 +1,2151 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Data/mathml3-strict-content.xsd b/Data/mathml3-strict-content.xsd new file mode 100755 index 0000000..869de61 --- /dev/null +++ b/Data/mathml3-strict-content.xsd @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Data/mathml3.xsd b/Data/mathml3.xsd new file mode 100755 index 0000000..283c31e --- /dev/null +++ b/Data/mathml3.xsd @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/MathML.psd1 b/MathML.psd1 new file mode 100644 index 0000000..0cd2e68 --- /dev/null +++ b/MathML.psd1 @@ -0,0 +1,147 @@ +# +# Module manifest for module 'MathML' +# +# Generated by: PowerPi +# +# Generated on: 10/1/2025 +# + +@{ + +# Script module or binary module file associated with this manifest. +RootModule = 'MathML.psm1' + +# Version number of this module. +ModuleVersion = '0.1' + +# Supported PSEditions +# CompatiblePSEditions = @() + +# ID used to uniquely identify this module +GUID = '71642e50-216c-4340-b02b-f50cd8a84dca' + +# Author of this module +Author = 'James Brundage' + +# Company or vendor of this module +CompanyName = 'Start-Automating' + +# Copyright statement for this module +Copyright = '2025 Start-Automating' + +# Description of the functionality provided by this module +Description = 'MathML and PowerShell' + +# Minimum version of the PowerShell engine required by this module +# PowerShellVersion = '' + +# Name of the PowerShell host required by this module +# PowerShellHostName = '' + +# Minimum version of the PowerShell host required by this module +# PowerShellHostVersion = '' + +# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# DotNetFrameworkVersion = '' + +# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. +# ClrVersion = '' + +# Processor architecture (None, X86, Amd64) required by this module +# ProcessorArchitecture = '' + +# Modules that must be imported into the global environment prior to importing this module +# RequiredModules = @() + +# Assemblies that must be loaded prior to importing this module +# RequiredAssemblies = @() + +# Script files (.ps1) that are run in the caller's environment prior to importing this module. +# ScriptsToProcess = @() + +# Type files (.ps1xml) to be loaded when importing this module +TypesToProcess = @('MathML.types.ps1xml') + +# Format files (.ps1xml) to be loaded when importing this module +# FormatsToProcess = @() + +# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess +# NestedModules = @() + +# 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. +FunctionsToExport = 'Get-MathML','Export-MathML', 'Import-MathML' + +# 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. +CmdletsToExport = '*' + +# Variables to export from this module +VariablesToExport = '*' + +# 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. +AliasesToExport = 'MathML' + +# DSC resources to export from this module +# DscResourcesToExport = @() + +# List of all modules packaged with this module +# ModuleList = @() + +# List of all files packaged with this module +# FileList = @() + +# 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. +PrivateData = @{ + + PSData = @{ + + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('Math', 'MathML') + + # A URL to the license for this module. + LicenseURI = 'https://github.com/PowerShellWeb/MathML/blob/main/LICENSE' + + # A URL to the main website for this project. + ProjectUri = 'https://github.com/PowerShellWeb/MathML' + + # A URL to an icon representing this module. + # IconUri = '' + + # ReleaseNotes of this module + ReleaseNotes = @' +## MathML 0.1 + +* Initial release of MathML (#1) +* Core Commands: + * Get-MathML (#2) + * Export-MathML (#3) + * Import-MathML (#4) +* Extended Types + * `MathML.get_SVG` (#5) + * `MathML.ToString()` (#8) + * `MathML.get/set_ID` (#9) +* MathML schemas included (#7) +* Initial Workflow (#6) + +'@ + + # Prerelease string of this module + # Prerelease = '' + + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false + + # External dependent modules of this module + # ExternalModuleDependencies = @() + + } # End of PSData hashtable + +} # End of PrivateData hashtable + +# HelpInfo URI of this module +# HelpInfoURI = '' + +# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. +# DefaultCommandPrefix = '' + +} + diff --git a/MathML.psm1 b/MathML.psm1 new file mode 100644 index 0000000..41a1efb --- /dev/null +++ b/MathML.psm1 @@ -0,0 +1,36 @@ +$commandsPath = Join-Path $PSScriptRoot Commands +:ToIncludeFiles foreach ($file in (Get-ChildItem -Path "$commandsPath" -Filter "*-*" -Recurse)) { + if ($file.Extension -ne '.ps1') { continue } # Skip if the extension is not .ps1 + foreach ($exclusion in '\.[^\.]+\.ps1$') { + if (-not $exclusion) { continue } + if ($file.Name -match $exclusion) { + continue ToIncludeFiles # Skip excluded files + } + } + . $file.FullName +} + +$myModule = $MyInvocation.MyCommand.ScriptBlock.Module +$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule) +$myModule.pstypenames.insert(0, $myModule.Name) + +New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore + +if ($home) { + $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name + if (-not (Test-Path $MyModuleProfileDirectory)) { + $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force + } + New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore +} + +# Set a script variable of this, set to the module +# (so all scripts in this scope default to the correct `$this`) +$script:this = $myModule + +#region Custom +#endregion Custom + +Export-ModuleMember -Alias * -Function * -Variable $myModule.Name + + diff --git a/MathML.types.ps1xml b/MathML.types.ps1xml new file mode 100644 index 0000000..15087ec --- /dev/null +++ b/MathML.types.ps1xml @@ -0,0 +1,60 @@ + + + + MathML + + + ToString + + + + ID + + <# +.SYNOPSIS + Gets a MathML id +.DESCRIPTION + Gets the ID attribute on a MathML element. + + MathML does not need to have an identifier, but it certainly can help. +#> +return $this.Math.id + + + <# +.SYNOPSIS + Sets a MathML id +.DESCRIPTION + Sets the ID attribute on a MathML element. + + MathML does not need to have an identifier, but it certainly can help. +#> +$this.Math.setAttribute("id", "$args") + + + + SVG + + <# +.SYNOPSIS + Gets MathML as SVG +.DESCRIPTION + Gets a MathML equation within an SVG +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/foreignObject +#> +[xml]@" +<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" font-size="3em"> + <foreignObject x="0%" y="0%" width="100%" height="100%"> + $($this.OuterXml) + </foreignObject> +</svg> +"@ + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 2f2f119..82dcf3c 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,71 @@ # MathML -PowerShell and MathML + +MathML is an XML stanard for representing mathematics, and a part of HTML5. + +## MathML Module + +This module allows you to get MathML from anywhere and work with it as an object. + +### Installing and Importing + +MathML can be installed from the PowerShell gallery + +~~~PowerShell +Install-Module MathML +~~~ + +After installation, you can import it like any module: + +~~~PowerShell +Import-Module MathML +~~~ + +### Getting MathML + +We can use `Get-MathML` (alias `MathML`) to extract MathML from a source + +~~~PowerShell +MathML https://dlmf.nist.gov/2.1 +~~~ + +This works for Wikipedia as well: + +~~~PowerShell +$roseMath = MathML 'https://en.wikipedia.org/wiki/Rose_(mathematics)' +$roseMath +~~~ + +We can also pass MathML directly in + +~~~PowerShell +MathML " + + + 1 + + + 1 + = + 2 + + +" +~~~ + + +We can get any previously loaded MathML by running Get-MathML with no parameters + +~~~PowerShell +MathML +~~~ + +## Future Goals + +MathML offers a unique opportunity for metaprogramming. + +In theory, expressions in most programming languages, including PowerShell, could be written as MathML. + +Thus one future goal is to provide translation from languages to MathML. + +More interestingly, MathML could also represent a "base language" used to reconstruct expressions in other languages. + +Thus the other major future goal is provide translation from MathML into various programming languages. \ No newline at end of file diff --git a/Types/MathML/ToString.ps1 b/Types/MathML/ToString.ps1 new file mode 100644 index 0000000..0d64886 --- /dev/null +++ b/Types/MathML/ToString.ps1 @@ -0,0 +1 @@ +$this.svg.OuterXml diff --git a/Types/MathML/get_ID.ps1 b/Types/MathML/get_ID.ps1 new file mode 100644 index 0000000..f668899 --- /dev/null +++ b/Types/MathML/get_ID.ps1 @@ -0,0 +1,9 @@ +<# +.SYNOPSIS + Gets a MathML id +.DESCRIPTION + Gets the ID attribute on a MathML element. + + MathML does not need to have an identifier, but it certainly can help. +#> +return $this.Math.id \ No newline at end of file diff --git a/Types/MathML/get_SVG.ps1 b/Types/MathML/get_SVG.ps1 new file mode 100644 index 0000000..7801a13 --- /dev/null +++ b/Types/MathML/get_SVG.ps1 @@ -0,0 +1,15 @@ +<# +.SYNOPSIS + Gets MathML as SVG +.DESCRIPTION + Gets a MathML equation within an SVG +.LINK + https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Element/foreignObject +#> +[xml]@" + + + $($this.OuterXml) + + +"@ \ No newline at end of file diff --git a/Types/MathML/set_ID.ps1 b/Types/MathML/set_ID.ps1 new file mode 100644 index 0000000..96f7c93 --- /dev/null +++ b/Types/MathML/set_ID.ps1 @@ -0,0 +1,9 @@ +<# +.SYNOPSIS + Sets a MathML id +.DESCRIPTION + Sets the ID attribute on a MathML element. + + MathML does not need to have an identifier, but it certainly can help. +#> +$this.Math.setAttribute("id", "$args") \ No newline at end of file