Skip to content

Commit 4e0c2b5

Browse files
author
James Brundage
committed
feat: Get-Htmx ( Fixes #6 )
1 parent 4042e70 commit 4e0c2b5

File tree

1 file changed

+196
-0
lines changed

1 file changed

+196
-0
lines changed

Commands/Get-Htmx.ps1

+196
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
function Get-HTMX {
2+
<#
3+
.SYNOPSIS
4+
Creates HTMX tags in PowerShell.
5+
.DESCRIPTION
6+
Get-Htmx is a freeform function that creates HTML tags in PowerShell.
7+
8+
The function has no explicit parameters.
9+
10+
Instead, broadly speaking, arguments become attributes (unless it appears to be a tag) and inputs become children.
11+
.NOTES
12+
Ideally, this command is very forgiving in its input and helps you write HTMX tag in PowerShell.
13+
14+
If this proves not to be the case, feel free to open an issue.
15+
.EXAMPLE
16+
Get-Htmx div class=container "Hello, World!"
17+
<div class="container">Hello, World!</div>
18+
.EXAMPLE
19+
htmx button "Click Me" hx-get=api/endpoint hx-trigger=click
20+
#>
21+
[ArgumentCompleter({
22+
param(
23+
$wordToComplete,
24+
$commandAst,
25+
$cursorPosition
26+
)
27+
})]
28+
29+
param()
30+
31+
# Collect all input
32+
$allInput = @($input)
33+
# and any arguments
34+
$allArguments = @($args)
35+
# Unroll any arguments: any collections are expanded to their elements
36+
$unrolledArguments = @($allArguments | . { process { $_ } })
37+
# Create a list of child elements
38+
$moreChildren = [Collections.Generic.List[Object]]::new()
39+
# Add all input to the list of child elements
40+
$moreChildren.AddRange($allInput)
41+
42+
# Create a collection of attributes
43+
$attributes = [Ordered]@{}
44+
45+
# And a list of all future content.
46+
$allContent = [Collections.Generic.List[Object]]::new()
47+
$allContent.AddRange($allInput)
48+
49+
# Capture the invocation information, as it will be easier to debug with it.
50+
$myInv = $MyInvocation
51+
52+
# If this function is called any other name, we will use that name as the tag name.
53+
# To do this, we use a relatively simple pattern.
54+
# Put in english, instead of Regex, it is:
55+
# * Optionally Match Get, followed by one or more punctuation characters
56+
# * Match HTMX, followed by zero or more punctuation characters
57+
$getHtmxPrefixPattern = '(?>Get\p{P}+)?HTMX[\p{P}]{0,}'
58+
$myName = $MyInvocation.InvocationName -replace $getHtmxPrefixPattern
59+
60+
# Now we walk over each argument and determine if it is an attribute or content.
61+
foreach ($argument in $unrolledArguments) {
62+
63+
# Starting easy, if the argument is a dictionary, we will treat it as attributes.
64+
if ($argument -is [Collections.IDictionary]) {
65+
foreach ($key in $argument.Keys) {
66+
$attributes[$key] = $argument[$key]
67+
}
68+
continue
69+
}
70+
71+
# If the argument is a string, and we have not yet determined the tag name, we will use it.
72+
if (
73+
-not $myName -and
74+
$argument -is [string] -and
75+
# provided, of course, that it does not look like a tag or an attribute
76+
$argument -notmatch '[\p{P}\<\>\s-[\-]]' -and $argument -notmatch '^hx-'
77+
) {
78+
$myName = $argument
79+
continue
80+
}
81+
82+
83+
# If the argument is _only_ whitespace and a colon or equals sign, we will skip it.
84+
if ($argument -is [string] -and
85+
$argument -match '^\s{0,}[=:]\s{0,}$') {
86+
continue
87+
}
88+
89+
# If the argument has a equals sign, surrounded by content, we will treat it as an attribute.
90+
if ($argument -is [string] -and $argument -match '^.+?=.+?$') {
91+
$key, $value = $argument -split '=', 2
92+
$attributes[$key] = $value
93+
continue
94+
}
95+
96+
# If the argument is a tag, we will add it to the content.
97+
if ($argument -match '[\<\>]') {
98+
$allContent.Add($argument)
99+
continue
100+
}
101+
102+
# If we have any attribute without a value, we will treat the next argument as the value.
103+
if ($attributes.Count -and $null -eq $attributes[-1]) {
104+
$lastKey = @($attributes.Keys)[-1]
105+
# If the last key is content, child, or children, we will treat the argument as a child.
106+
if ($lastKey -in 'content', 'child', 'children') {
107+
$moreChildren.Add($argument)
108+
$attributes.RemoveAt($lastKey)
109+
} else {
110+
# Otherwise, we will treat it as a value.
111+
$attributes[@($attributes.Keys)[-1]] = $argument
112+
}
113+
114+
} else {
115+
# Otherwise, if the argument is whitespace, we will add it to the content.
116+
if ($argument -match '[\s\r\n]') {
117+
$allContent.Add($argument)
118+
} else {
119+
# and if it does not we will treat it as an attribute name.
120+
$attributes[$argument -replace '^[\p{P}]+'] = $null
121+
}
122+
}
123+
}
124+
125+
126+
# If we have no attributes and no children, we will return the module.
127+
if (-not $attributes.Count -and -not $moreChildren.Count) {
128+
return $HtmxPS
129+
}
130+
131+
# Otherwise, we will create the tag.
132+
$ElementName = if ($myName) { $myName } else { "html" }
133+
134+
@(
135+
"<$ElementName"
136+
# We will walk over each attribute and create the attribute string.
137+
foreach ($attr in $attributes.GetEnumerator()) {
138+
if (-not $attr.Key) { continue }
139+
if (-not [String]::IsNullOrEmpty($attr.Value)) {
140+
" $($attr.Key)=`"$([Web.HttpUtility]::HtmlAttributeEncode($attr.Value))`""
141+
} else {
142+
" $($attr.Key)"
143+
}
144+
}
145+
146+
# If we do not have children, we can close the tag now.
147+
if (-not $allContent) { ' />'}
148+
# Otherwise, we will close the tag after the children.
149+
else { '>'}
150+
151+
if ($allContent) {
152+
@(
153+
# We will walk over each child and create the child string.
154+
foreach ($contentItem in $allContent) {
155+
# If the content has a `ToHtml` method, we will use it.
156+
if ($contentItem.ToHtml.Invoke) {
157+
$contentItem.ToHtml()
158+
} elseif ($contentItem.Html) {
159+
# Otherwise, we will look for an `Html` property.
160+
$contentItem.Html
161+
}
162+
elseif ($contentItem.outerHTML) {
163+
# Or an `outerHTML` property.
164+
$contentItem.outerHTML
165+
} elseif ($contentItem.OuterXML) {
166+
# Or an `OuterXML` property.
167+
$contentItem.OuterXML
168+
} else {
169+
# If none of those are available, we will use the content as is.
170+
$contentItem
171+
}
172+
}
173+
"</$ElementName>"
174+
) -join ''
175+
}
176+
) -join ''
177+
}
178+
179+
# We have to register argument completers for the command and it's noun.
180+
$commandName = 'Get-HTMX'
181+
$CommandNameAndAliases = @(
182+
$commandName
183+
if ($commandName -match '^Get\p{P}+') {
184+
$($commandName -replace 'Get\p{P}+')
185+
}
186+
)
187+
# we do this by taking the argument completer attribute on ourself
188+
foreach ($attributes in $ExecutionContext.SessionState.InvokeCommand.GetCommand($commandName,'Function').ScriptBlock.Attributes) {
189+
if ($attributes -is [ArgumentCompleter]) {
190+
# and registering it for each command name and alias.
191+
foreach ($commandToComplete in $CommandNameAndAliases) {
192+
Register-ArgumentCompleter -CommandName $commandToComplete -ScriptBlock $attributes.ScriptBlock
193+
}
194+
break
195+
}
196+
}

0 commit comments

Comments
 (0)