Examples/TurtleShell.html.ps1
|
param( [string] $Lucky = @' turtle lucky '@, [PSObject[]] $Examples = @( # Any example code to render. # Examples can use the command attribute to specify the command they run. "<details name='examples'>" "<summary>Examples</summary>" #region Basic Examples @" <button class='reptile' id='circle' command='turtle circle 42 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Circle</button> <button class='reptile' id='pie' command='turtle rotate (randomangle) pie 42 3 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Pie</button> <button class='reptile' id='square' command='turtle rotate (randomangle) square 42 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Square</button> <button class='reptile' id='rectangle' command='turtle rotate (randomangle) rectangle 42 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Rectangle</button> <button class='reptile' id='star' command='turtle rotate (randomangle) star 42 (5,6,7,8,9 | Get-Random) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Star</button> <button class='reptile' id='flower' command='turtle rotate (randomangle) flower 42 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Flower</button> <button class='reptile' id='starflower' command='turtle rotate (randomangle) starflower 42 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)'>Starflower</button> <button class='reptile' id='stepspiral' command='turtle rotate (randomangle) stepspiral fill random random stroke random random'>Stepspiral</button> "@ #endregion Basic Examples #region Sector Examples " <blockquote> <details open><summary>Sectors</summary> " @" <button class='reptile' id='quadrants' command="turtle rotate (randomangle) @( 'CircleArc',42, 90, 'Rotate', 90 * 4 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Quadrants</button> <button class='reptile' id='antiquadrants' command="turtle rotate (randomangle) @( 'CircleArc',42, -90, 'Rotate', 90 * 4 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Antiquadrants</button> <button class='reptile' id='sextants' command="turtle rotate (randomangle) @( 'CircleArc',42, 60, 'Rotate', 60 * 6 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Sextants</button> <button class='reptile' id='antisextants' command="turtle rotate (randomangle) @( 'CircleArc',42, -60, 'Rotate', 60 * 6 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Antisextants</button> <button class='reptile' id='octants' command="turtle rotate (randomangle) @( 'CircleArc',42, 45, 'Rotate', 45 * 8 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Octants</button> <button class='reptile' id='antioctants' command="turtle rotate (randomangle) @( 'CircleArc',42, -45, 'Rotate', 45 * 8 ) fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor)">Antioctants</button> "@ " </details> </blockquote> " #endregion Sector Examples #region Pie Examples "<blockquote>" "<details open><summary>Pies</summary>" @" <button class='reptile' id='pie3' command=" turtle rotate (randomangle) pie 42 3 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">3 slices</button> <button class='reptile' id='pie4' command=" turtle rotate (randomangle) pie 42 4 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">4 slices</button> <button class='reptile' id='pie5' command=" turtle rotate (randomangle) pie 42 5 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">5 slices</button> <button class='reptile' id='pie6' command=" turtle rotate (randomangle) pie 42 6 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">6 slices</button> <button class='reptile' id='pie7' command=" turtle rotate (randomangle) pie 42 7 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">7 slices</button> <button class='reptile' id='pie8' command=" turtle rotate (randomangle) pie 42 8 fill (randomcolor) (randomcolor) stroke (randomcolor) (randomcolor) ">8 slices</button> "@ "</details>" "</blockquote>" #endregion Pie Examples #region Polygon Examples "<blockquote>" "<details open><summary>Polygons</summary>" @" <button class='reptile' id='polygon3' command=" turtle rotate polygon 42 3 fill random random stroke random random ">Triangle</button> <button class='reptile' id='polygon4' command=" turtle rotate polygon 42 4 fill random random stroke random random ">Square</button> <button class='reptile' id='polygon5' command=" turtle rotate polygon 42 5 fill random random stroke random random ">Pentagon</button> <button class='reptile' id='polygon6' command=" turtle rotate polygon 42 6 fill random random stroke random random ">Hexagon</button> <button class='reptile' id='polygon7' command=" turtle rotate polygon 42 7 fill random random stroke random random ">Septagon</button> <button class='reptile' id='polygon8' command=" turtle rotate polygon 42 8 fill random random stroke random random ">Octagon</button> <button class='reptile' id='polygon9' command=" turtle rotate polygon 42 9 fill random random stroke random random ">Nonagon</button> "@ "</details>" "</blockquote>" #endregion Pie Examples ) ) "<html>" "<head>" "</head>" "<body>" "<style>" " .invisible { display: none } " # Repl Input Grid # A repl's input can be thought of as a simple grid: " .repl-command-grid { display: grid; text-align: center; grid-template-areas: 'variables' 'command' 'go'; place-content: center; place-items: center; grid-template-rows: auto auto auto; grid-template-columns: 1fr; gap: 0.5rem; padding: 0.5rem; } " " .repl-command { grid-area: command; width: 100%; } .repl-go { grid-area: go; width: 50%; text-align: center; } .repl-variables { grid-area: variables; width: 100%; } " " " "body { background-color: black; max-width: 100vw; height: 100vh; color: white; }" ".outputGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 2.5em; margin: 2.5em; place-self: center; place-items: center; }" "table { width: 100%; border: 1px solid; }" "th, td { border: 1px solid;}" "output { display: block; }" "button, summary { font-size: 1.25rem; padding: 0.25rem; }" @" .current-tabs { position: sticky; top: 1%; left: 1%; display: flex; flex: auto; flex-direction: column; } "@ "</style>" @" <menu class='tabs'> </menu> "@ " <div class='repl-command-grid'> <details class='invisible repl-variables'><summary>Variables</summary></details> <textarea class='repl-command' id='command' autocomplete='repl-command' rows='3' spellcheck='false'> $Lucky </textarea> <button class='repl-go' id='go'>Go Turtle!</button> </div> " "<menu>" "<menu>" @" <button class='reptile' id='feelinglucky' command="$([Web.HttpUtility]::HtmlAttributeEncode($Lucky))">Lucky</button> "@ "</menu>" "<menu>" $Examples "</menu>" "</details>" "<output id='output'></output>" "<ol id='output-item-list'>" "</ol>" $scaleAnimation = "{ scale: ['100%', '105%','100%'] }, 67" $TurtleShellJS = [Ordered]@{ "getVariables" = "function(inputId) { const inputElement = document.getElementById(inputId) if (! inputElement) { return } if (! inputElement.previousElementSibling) { return } const variables = {} for (const element of [ ...inputElement.previousElementSibling.childNodes ]) { if (element === inputElement) { continue } if (element.name && element.value) { variables[element.name] = element.value } } return variables } " "onInput" = "function() { console.log(this.value) // First we need to find all of the matching variable names const toAdd = [] for (const match of [ ...this?.value?.matchAll(/(?:\:|-{2}|\$|\@)(?<name>\w+)/g) ]) { toAdd.push(match.groups.name) } // If the length was zero if (toAdd.length == 0) { // make the previous sibling invisible this.previousElementSibling.classList.add('invisible') return } // Then we need to figure out what changed const newVariables = [] const removedVariables = [] const uniqueVariables = [] if (this.dataset?.variableNames) { if (this.dataset?.variableNames != toAdd.join(' ')) { var oldNames = this.dataset?.variableNames.split(' ') for (let index = 0; index < toAdd.length; index++) { if (index < oldNames.length) { if (oldNames[index] != toAdd[index]) { uniqueVariables.push({ name:toAdd[index], new:false, old: oldNames[index] }) } else { uniqueVariables.push({ name:toAdd[index], new:false }) } } else { uniqueVariables.push({name:toAdd[index], new:true}) } } } } else { for (const variableName of toAdd) { uniqueVariables.push({name:variableName, new:true}) } } this.dataset['variableNames'] = toAdd.join(' ') this.previousElementSibling.classList.remove('invisible') for (const uniqueVariable of uniqueVariables) { if (uniqueVariable.new) { // new variable, create a new element const newInput = document.createElement('input') newInput.name = uniqueVariable.name newInput.id = this.id + '-' + uniqueVariable.name const newLabel = document.createElement('label') newLabel.setAttribute('for', newInput.id) newLabel.innerText = uniqueVariable.name this.previousElementSibling.appendChild(newLabel) this.previousElementSibling.appendChild(newInput) } if (uniqueVariable.old) { // Variable with old name, rename const oldId = this.id + '-' + uniqueVariable.old const newId = this.id + '-' + uniqueVariable.name for (const childNode of [ ...this.previousElementSibling.childNodes ]) { if (childNode.name == uniqueVariable.old) { childNode.setAttribute('name', uniqueVariable.name) childNode.setAttribute('id', newId) } if (childNode.getAttribute('for') == oldId) { childNode.setAttribute('for', newId) childNode.innerText = uniqueVariable.name } } } } console.log(uniqueVariables) }" "go" = @" async function() { let inputId = '' let inputScript = '' if (event?.target?.animate) { event.target.animate($scaleAnimation); } if ( event?.target?.getAttribute && event?.target?.getAttribute('command') ) { inputScript = event.target.getAttribute('command') inputId = turtleShell.newShell(inputScript) const outputId = inputId.replace(/^command/i, 'output') const out = document.getElementById(outputId) const response = await fetch( window.location.href, {method: 'POST',body: inputScript} ) out.innerHTML = await response.text() out.animate({ scale: ['0%', '100%'] }, 67); const inputElement = document.getElementById(inputId) if (inputElement) { inputElement.removeAttribute('disabled') } const goElement = document.getElementById( inputId.replace(/^command/i, 'go') ) if (goElement) { goElement.removeAttribute('disabled') } return } if (event?.target?.previousSibling?.value && event?.target?.previousSibling?.id.match(/^command/i)) { inputId = event?.target?.previousSibling?.id const outputId = inputId.replace(/^command/i, 'output') } if (! inputScript && inputId == 'command' || ! inputId) { const repl = document.getElementById('command') repl.animate($scaleAnimation) inputId = turtleShell.newShell( repl.value, turtleShell.getVariables(repl.id) ) document.getElementById(inputId).addEventListener('input',turtleShell.onInput) inputScript = repl.value } else { const repl = document.getElementById(inputId) repl.setAttribute('disabled', 'true') inputScript = repl?.value } if (! inputScript) { return } const outputId = inputId.replace(/^command/i, 'output') const out = document.getElementById(outputId) const goElement = document.getElementById( inputId.replace(/^command/i, 'go') ) if (goElement) { goElement.setAttribute('disabled', 'true') } const inputElement = document.getElementById(inputId) const requestBody = { command: inputScript } if (inputElement) { inputElement.animate($scaleAnimation); const variables = turtleShell.getVariables(inputId) if (variables) { for (const variableName of Object.keys(variables)) { requestBody[variableName] = variables[variableName] } } } const response = await fetch(window.location.href, { headers: {"Content-Type": "application/json"}, method: 'POST', body: JSON.stringify(requestBody) } ) out.innerHTML = await response.text() out.animate({ scale: ['0%', '100%'] }, 67); if (inputElement) { inputElement.removeAttribute('disabled') inputElement.animate($scaleAnimation); } if (goElement) { goElement.removeAttribute('disabled') } } "@ "newShell" = @" function (input, variables = {}) { const now = new Date() const outputItemList = document.getElementById('output-item-list') const newListDetails = document.createElement('details') newListDetails.setAttribute('open', '') const newListSummary = document.createElement('summary') newListSummary.innerText = outputItemList.childNodes.length newListDetails.appendChild(newListSummary) const newListGrid = document.createElement('div') newListGrid.classList.add('repl-command-grid') newListDetails.appendChild(newListGrid) const newListVariableArea = document.createElement('details') const newListVariableSummary = document.createElement('summary') newListVariableSummary.innerText = 'variables' if (! variables || Object.keys(variables).length == 0) { newListVariableArea.classList.add('invisible') } newListVariableArea.classList.add('repl-variables') for (const variableName of Object.keys(variables)) { const newInput = document.createElement('input') newInput.name = variableName newInput.value = variables[variableName] newInput.id = 'command' + now.getTime() + '-' + variableName const newLabel = document.createElement('label') newLabel.setAttribute('for', newInput.id) newLabel.innerText = variableName newListVariableArea.appendChild(newLabel) newListVariableArea.appendChild(newInput) } newListVariableArea.appendChild(newListVariableSummary) newListGrid.appendChild(newListVariableArea) const newListInput = document.createElement('textarea') const inputLines = input.split(/(\r\n|\n|\r)/) newListInput.setAttribute('spellcheck','false') newListInput.setAttribute('autocomplete','repl-command') newListInput.setAttribute('rows',inputLines.length - 1) newListInput.setAttribute('disabled', 'true') newListInput.classList.add('repl-command') newListInput.id = 'command' + now.getTime() newListInput.value = input newListGrid.appendChild(newListInput) const newListOutput = document.createElement('output') newListOutput.id = newListInput.id.replace(/^command/i, 'output') const newGoButton = document.createElement('button') newGoButton.id = newListInput.id.replace(/^command/i, 'go') newGoButton.innerText = 'Go Turtle' newGoButton.classList.add('repl-go') newGoButton.addEventListener('click', this.go) newGoButton.setAttribute('disabled', 'true') newListGrid.appendChild(newGoButton) newListDetails.appendChild(newListOutput) const topToBottom = true; if (topToBottom && outputItemList.firstChild) { outputItemList.insertBefore(newListDetails, outputItemList.firstChild) } else { outputItemList.appendChild(newListDetails) } newListDetails.animate({ scale: ['0%', '100%'] }, 67); return newListInput.id } "@ } "<script type='module'>" @" const turtleShell = { $( @(foreach ($key in $TurtleShellJS.Keys) { "$($key): $($TurtleShellJS[$key])" }) -join (',' + [Environment]::NewLine + (' ' * 4)) ) } "@ @" for (const replInput of [ ...document.querySelectorAll('.repl-command') ]) { replInput.addEventListener('input', turtleShell.onInput) } for (const goButton of [ ...document.querySelectorAll('.reptile') ]) { goButton.addEventListener('click', turtleShell.go) } document.getElementById('go').addEventListener('click', turtleShell.go) "@ "" "</script>" "</body>" "</html>" |