-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
James Brundage
committed
Oct 23, 2024
1 parent
4042e70
commit 4e0c2b5
Showing
1 changed file
with
196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,196 @@ | ||
function Get-HTMX { | ||
<# | ||
.SYNOPSIS | ||
Creates HTMX tags in PowerShell. | ||
.DESCRIPTION | ||
Get-Htmx is a freeform function that creates HTML tags in PowerShell. | ||
The function has no explicit parameters. | ||
Instead, broadly speaking, arguments become attributes (unless it appears to be a tag) and inputs become children. | ||
.NOTES | ||
Ideally, this command is very forgiving in its input and helps you write HTMX tag in PowerShell. | ||
If this proves not to be the case, feel free to open an issue. | ||
.EXAMPLE | ||
Get-Htmx div class=container "Hello, World!" | ||
<div class="container">Hello, World!</div> | ||
.EXAMPLE | ||
htmx button "Click Me" hx-get=api/endpoint hx-trigger=click | ||
#> | ||
[ArgumentCompleter({ | ||
param( | ||
$wordToComplete, | ||
$commandAst, | ||
$cursorPosition | ||
) | ||
})] | ||
|
||
param() | ||
|
||
# Collect all input | ||
$allInput = @($input) | ||
# and any arguments | ||
$allArguments = @($args) | ||
# Unroll any arguments: any collections are expanded to their elements | ||
$unrolledArguments = @($allArguments | . { process { $_ } }) | ||
# Create a list of child elements | ||
$moreChildren = [Collections.Generic.List[Object]]::new() | ||
# Add all input to the list of child elements | ||
$moreChildren.AddRange($allInput) | ||
|
||
# Create a collection of attributes | ||
$attributes = [Ordered]@{} | ||
|
||
# And a list of all future content. | ||
$allContent = [Collections.Generic.List[Object]]::new() | ||
$allContent.AddRange($allInput) | ||
|
||
# Capture the invocation information, as it will be easier to debug with it. | ||
$myInv = $MyInvocation | ||
|
||
# If this function is called any other name, we will use that name as the tag name. | ||
# To do this, we use a relatively simple pattern. | ||
# Put in english, instead of Regex, it is: | ||
# * Optionally Match Get, followed by one or more punctuation characters | ||
# * Match HTMX, followed by zero or more punctuation characters | ||
$getHtmxPrefixPattern = '(?>Get\p{P}+)?HTMX[\p{P}]{0,}' | ||
$myName = $MyInvocation.InvocationName -replace $getHtmxPrefixPattern | ||
|
||
# Now we walk over each argument and determine if it is an attribute or content. | ||
foreach ($argument in $unrolledArguments) { | ||
|
||
# Starting easy, if the argument is a dictionary, we will treat it as attributes. | ||
if ($argument -is [Collections.IDictionary]) { | ||
foreach ($key in $argument.Keys) { | ||
$attributes[$key] = $argument[$key] | ||
} | ||
continue | ||
} | ||
|
||
# If the argument is a string, and we have not yet determined the tag name, we will use it. | ||
if ( | ||
-not $myName -and | ||
$argument -is [string] -and | ||
# provided, of course, that it does not look like a tag or an attribute | ||
$argument -notmatch '[\p{P}\<\>\s-[\-]]' -and $argument -notmatch '^hx-' | ||
) { | ||
$myName = $argument | ||
continue | ||
} | ||
|
||
|
||
# If the argument is _only_ whitespace and a colon or equals sign, we will skip it. | ||
if ($argument -is [string] -and | ||
$argument -match '^\s{0,}[=:]\s{0,}$') { | ||
continue | ||
} | ||
|
||
# If the argument has a equals sign, surrounded by content, we will treat it as an attribute. | ||
if ($argument -is [string] -and $argument -match '^.+?=.+?$') { | ||
$key, $value = $argument -split '=', 2 | ||
$attributes[$key] = $value | ||
continue | ||
} | ||
|
||
# If the argument is a tag, we will add it to the content. | ||
if ($argument -match '[\<\>]') { | ||
$allContent.Add($argument) | ||
continue | ||
} | ||
|
||
# If we have any attribute without a value, we will treat the next argument as the value. | ||
if ($attributes.Count -and $null -eq $attributes[-1]) { | ||
$lastKey = @($attributes.Keys)[-1] | ||
# If the last key is content, child, or children, we will treat the argument as a child. | ||
if ($lastKey -in 'content', 'child', 'children') { | ||
$moreChildren.Add($argument) | ||
$attributes.RemoveAt($lastKey) | ||
} else { | ||
# Otherwise, we will treat it as a value. | ||
$attributes[@($attributes.Keys)[-1]] = $argument | ||
} | ||
|
||
} else { | ||
# Otherwise, if the argument is whitespace, we will add it to the content. | ||
if ($argument -match '[\s\r\n]') { | ||
$allContent.Add($argument) | ||
} else { | ||
# and if it does not we will treat it as an attribute name. | ||
$attributes[$argument -replace '^[\p{P}]+'] = $null | ||
} | ||
} | ||
} | ||
|
||
|
||
# If we have no attributes and no children, we will return the module. | ||
if (-not $attributes.Count -and -not $moreChildren.Count) { | ||
return $HtmxPS | ||
} | ||
|
||
# Otherwise, we will create the tag. | ||
$ElementName = if ($myName) { $myName } else { "html" } | ||
|
||
@( | ||
"<$ElementName" | ||
# We will walk over each attribute and create the attribute string. | ||
foreach ($attr in $attributes.GetEnumerator()) { | ||
if (-not $attr.Key) { continue } | ||
if (-not [String]::IsNullOrEmpty($attr.Value)) { | ||
" $($attr.Key)=`"$([Web.HttpUtility]::HtmlAttributeEncode($attr.Value))`"" | ||
} else { | ||
" $($attr.Key)" | ||
} | ||
} | ||
|
||
# If we do not have children, we can close the tag now. | ||
if (-not $allContent) { ' />'} | ||
# Otherwise, we will close the tag after the children. | ||
else { '>'} | ||
|
||
if ($allContent) { | ||
@( | ||
# We will walk over each child and create the child string. | ||
foreach ($contentItem in $allContent) { | ||
# If the content has a `ToHtml` method, we will use it. | ||
if ($contentItem.ToHtml.Invoke) { | ||
$contentItem.ToHtml() | ||
} elseif ($contentItem.Html) { | ||
# Otherwise, we will look for an `Html` property. | ||
$contentItem.Html | ||
} | ||
elseif ($contentItem.outerHTML) { | ||
# Or an `outerHTML` property. | ||
$contentItem.outerHTML | ||
} elseif ($contentItem.OuterXML) { | ||
# Or an `OuterXML` property. | ||
$contentItem.OuterXML | ||
} else { | ||
# If none of those are available, we will use the content as is. | ||
$contentItem | ||
} | ||
} | ||
"</$ElementName>" | ||
) -join '' | ||
} | ||
) -join '' | ||
} | ||
|
||
# We have to register argument completers for the command and it's noun. | ||
$commandName = 'Get-HTMX' | ||
$CommandNameAndAliases = @( | ||
$commandName | ||
if ($commandName -match '^Get\p{P}+') { | ||
$($commandName -replace 'Get\p{P}+') | ||
} | ||
) | ||
# we do this by taking the argument completer attribute on ourself | ||
foreach ($attributes in $ExecutionContext.SessionState.InvokeCommand.GetCommand($commandName,'Function').ScriptBlock.Attributes) { | ||
if ($attributes -is [ArgumentCompleter]) { | ||
# and registering it for each command name and alias. | ||
foreach ($commandToComplete in $CommandNameAndAliases) { | ||
Register-ArgumentCompleter -CommandName $commandToComplete -ScriptBlock $attributes.ScriptBlock | ||
} | ||
break | ||
} | ||
} |