Skip to content

Commit 5ba7473

Browse files
feat: OpenPackage.View.Help.html ( Fixes #205 )
1 parent 0eaf39e commit 5ba7473

1 file changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
<#
2+
.SYNOPSIS
3+
Views Help as html
4+
.DESCRIPTION
5+
Views the help for Commands as html.
6+
.NOTES
7+
This _can_ -InvokeExample.
8+
9+
You should not -InvokeExamples of commands you do not know and trust will output as html.
10+
#>
11+
param(
12+
# The command
13+
[PSObject]
14+
$Command,
15+
16+
# If set, will invoke examples.
17+
# This can be as dangerous as your examples.
18+
[switch]
19+
$InvokeExample,
20+
21+
# If set, will not show the command name
22+
[switch]
23+
$HideName
24+
)
25+
26+
# Try to get command help
27+
$CommandHelp = Get-Help -Name $Command -ErrorAction Ignore
28+
# If we cannot get help, return
29+
if (-not $CommandHelp) { return }
30+
31+
# If the help was a string
32+
if ($CommandHelp -is [string]) {
33+
# return preformatted text.
34+
return "<pre>$([Web.HttpUtility]::HtmlEncode($CommandHelp))</pre>"
35+
}
36+
37+
# Get the core parts of help we need.
38+
$synopsis = $CommandHelp.Synopsis
39+
$description = $CommandHelp.description.text -join [Environment]::NewLine
40+
$notes = $CommandHelp.alertSet.alert.text -join [Environment]::NewLine
41+
42+
filter looksLikeMarkdown {
43+
if (
44+
# If it's got a markdown-like link
45+
$_ -match '\[[^\]]+\]\(' -or
46+
# Or any of the lines start with markdown special characters
47+
$_ -split '(?>\r\n|\n)' -match '\s{0,3}[\#*~`]'
48+
) {
49+
# it's probably markdown
50+
$_
51+
}
52+
}
53+
54+
filter looksLikeTags {
55+
if ($_ -match '^\s{0,}<') {
56+
$_
57+
}
58+
}
59+
60+
# Display the command name, synopsis, and description
61+
if (-not $HideName) {
62+
"<h1>$([Web.HttpUtility]::HtmlEncode(
63+
$CommandHelp.Name -replace '(?:.+?/){0,}' -replace '\.ps1$' -replace '\..+?$'
64+
))</h1>"
65+
}
66+
67+
if ($synopsis) {
68+
if ($synopsis | looksLikeMarkdown) {
69+
(ConvertFrom-Markdown -InputObject $synopsis).Html
70+
}
71+
elseif ($synopsis | looksLikeTags) {
72+
$synopsis
73+
} else {
74+
if ($page -is [Collections.IDictionary] -and -not $page.Title) {
75+
$page.Title = $([Web.HttpUtility]::HtmlEncode($synopsis))
76+
}
77+
"<h2>$([Web.HttpUtility]::HtmlEncode($synopsis))</h2>"
78+
}
79+
}
80+
81+
if ($description) {
82+
if ($description | lookslikeMarkdown) {
83+
$markdown = (ConvertFrom-Markdown -InputObject $description).Html
84+
if ($page -is [Collections.IDictionary] -and -not $page.Description) {
85+
$xmarkdown = "<xhtml>$markdown</xhtml>"
86+
$page.Description = $xmarkdown.xhtml.innerText
87+
}
88+
89+
}
90+
elseif ($description | looksLikeTags) {
91+
$description
92+
}
93+
else {
94+
if ($page -is [Collections.IDictionary] -and -not $page.Description) {
95+
$page.Description = $description
96+
}
97+
"<h3>$([Web.HttpUtility]::HtmlEncode($description))</h3>"
98+
}
99+
}
100+
101+
if ($notes) {
102+
# If there were notes, convert them from markdown
103+
(ConvertFrom-Markdown -InputObject $notes).Html
104+
}
105+
106+
#region Grid Styles
107+
"<style>"
108+
".example-grid {
109+
width: 90vw;
110+
margin-left: 5vw;
111+
margin-right: 5vw;
112+
text-align: center;
113+
}"
114+
".example {
115+
text-align: center;
116+
}"
117+
".example-code {
118+
text-align: center;
119+
font-size: .9rem;
120+
margin-left: auto;
121+
margin-right: auto;
122+
}"
123+
124+
"code { text-align: left}"
125+
126+
".example-menu { text-align: right; margin-top: -.5rem; margin-bottom: -.5rem }"
127+
".copy-button { float: right; }"
128+
129+
".example-outputs {
130+
display: flex;
131+
flex-direction: column;
132+
align-items: center;
133+
justify-content: center;
134+
gap: 1rem;
135+
margin: 0.8rem;
136+
padding: 0.2rem;
137+
width: 90vw;
138+
}"
139+
".example-output { text-align: center }"
140+
"
141+
pre {
142+
position: relative;
143+
}
144+
"
145+
"</style>"
146+
#endregion Grid Styles
147+
148+
$exampleCount = @($CommandHelp.examples.example).Length
149+
$progress = [Ordered]@{id=Get-Random}
150+
$progress.Activity = "$($command) examples"
151+
# Create a grid for examples
152+
"<div class='example-grid'>"
153+
$exampleNumber = 0
154+
# Walk over each example
155+
foreach ($example in $CommandHelp.examples.example) {
156+
$exampleNumber++
157+
158+
# Combine the code and remarks
159+
$exampleLines =
160+
@(
161+
$example.Code
162+
foreach ($remark in $example.Remarks.text) {
163+
if (-not $remark) { continue }
164+
$remark
165+
}
166+
) -join ([Environment]::NewLine) -split '(?>\r\n|\n)' # and split into lines
167+
168+
# Anything until the first non-comment line is a markdown predicate to the example
169+
$nonCommentLine = $false
170+
$markdownLines = @()
171+
172+
$progress.PercentComplete = $exampleNumber * 100 / $exampleCount
173+
$progress.Activity = "$($command) examples $exampleNumber"
174+
$progress.Status = "$($exampleLines[0])"
175+
Write-Progress @progress
176+
177+
# Go thru each line in the example as part of a loop
178+
$codeBlock = @(foreach ($exampleLine in $exampleLines) {
179+
# Any comments until the first uncommentedLine are markdown
180+
if ($exampleLine -match '^\#' -and -not $nonCommentLine) {
181+
$markdownLines += $exampleLine -replace '^\#' -replace '^\s+'
182+
} else {
183+
$nonCommentLine = $true
184+
$exampleLine
185+
}
186+
}) -join [Environment]::NewLine
187+
188+
# Join all of our markdown lines together
189+
$Markdown = $markdownLines -join [Environment]::NewLine
190+
191+
# and start our example div
192+
"<div class='example'>"
193+
# If we had markdown, output it
194+
if ($markdown) {
195+
(ConvertFrom-Markdown -InputObject $Markdown).Html
196+
}
197+
# followed by our sample code
198+
"<div class='example-code'>"
199+
"<pre>"
200+
"<code class='language-powershell'>" +
201+
[Web.HttpUtility]::HtmlEncode($codeBlock) +
202+
"</code>"
203+
"</pre>"
204+
"</div>"
205+
206+
# If we do not want to invoke examples, we can continue to the next example.
207+
if (-not $InvokeExample) {
208+
"</div>"
209+
"<hr/>"
210+
continue
211+
}
212+
# Otherwise, try to make our example a script block
213+
$exampleCode =
214+
try {
215+
[scriptblock]::Create($codeBlock)
216+
} catch {
217+
Write-Warning "Unable to convert $($example.code) to a script"
218+
continue
219+
}
220+
221+
if (-not $global:ExampleOutputCache) {
222+
$global:ExampleOutputCache = [Ordered]@{}
223+
}
224+
if (-not $global:ExampleOutputCache[$codeBlock]) {
225+
$global:ExampleOutputCache[$codeBlock] = @(. $exampleCode)
226+
}
227+
# then run it and capture the output
228+
$exampleOutputs = $global:ExampleOutputCache[$codeBlock]
229+
230+
# Keep track of our example output count
231+
$exampleOutputNumber = 0
232+
# and start walking thru the list
233+
"<div class='example-outputs'>"
234+
foreach ($exampleOutput in $exampleOutputs) {
235+
$exampleOutputNumber++
236+
# Each output goes in a div
237+
"<div class='example-output'>"
238+
# if the output was a file
239+
if ($exampleOutput -is [IO.FileInfo]) {
240+
# and that file is SVG
241+
if ($exampleOutput.Extension -eq '.svg') {
242+
# include it inline.
243+
Get-Content $exampleOutput.FullName -Raw
244+
}
245+
} else {
246+
# If the output was a turtle object
247+
if ($exampleOutput.pstypenames -contains 'Turtle') {
248+
# set it's ID
249+
$exampleOutput.ID = "$($Command)-Example-$exampleCounter"
250+
if ($exampleOutputs.Length -gt 1) {
251+
# If we have more than out output,
252+
# attach our example counter
253+
$exampleOutput.ID += "-$($exampleOutputNumber)"
254+
}
255+
}
256+
# Include our example output inline
257+
if ($exampleOutput.ToHtml.Invoke) {
258+
$exampleOutput.ToHtml()
259+
} else {
260+
"$exampleOutput"
261+
}
262+
}
263+
"</div>"
264+
}
265+
"</div>"
266+
"<hr/>"
267+
"</div>"
268+
}
269+
"</div>"
270+
271+
$progress.Remove('PercentComplete')
272+
$progress['Completed'] = $true
273+
Write-Progress @progress
274+
275+
@"
276+
<script>
277+
document.querySelectorAll('pre > code').forEach(element => {
278+
const copyCodeButton = document.createElement('div')
279+
copyCodeButton.classList.add('copy-button')
280+
copyCodeButton.onclick = () => navigator.clipboard.writeText(element.innerText)
281+
copyCodeButton.innerHTML = ``$(
282+
Format-OpenPackage -View FeatherIcon.svg -Option @{icon='clipboard'}
283+
)``
284+
element.parentNode.prepend(copyCodeButton)
285+
});
286+
</script>
287+
"@

0 commit comments

Comments
 (0)