hyperscript.org
Version:
a small scripting language for the web
374 lines (331 loc) • 12.9 kB
JavaScript
var d=class t{errors=[];collectErrors(e){if(e||(e=new Set),e.has(this))return[];e.add(this);var o=[...this.errors];for(var n of Object.keys(this))for(var s of[this[n]].flat())s instanceof t&&o.push(...s.collectErrors(e));return o}sourceFor(){return this.programSource.substring(this.startToken.start,this.endToken.end)}lineFor(){return this.programSource.split(`
`)[this.startToken.line-1]}static parseEventArgs(e){var o=[];if(e.token(0).value==="("&&(e.token(1).value===")"||e.token(2).value===","||e.token(2).value===")")){e.matchOpToken("(");do o.push(e.requireTokenType("IDENTIFIER"));while(e.matchOpToken(","));e.requireOpToken(")")}return o}};var c=class extends d{constructor(){super(),this.constructor.keyword&&(this.type=this.constructor.keyword+"Command")}execute(e){return e.meta.command=this,e.meta.runtime.unifiedExec(this,e)}findNext(e){return e.meta.runtime.findNext(this,e)}};var a=class t extends c{static keyword="breakpoint";static parse(e){if(e.matchToken("breakpoint"))return new t}resolve(e){var o;globalThis.hdb=o=new r(e,e.meta.runtime,this);try{return o.break(e)}catch(n){console.error(n,n.stack)}}};function r(t,e,o){this.ctx=t,this.runtime=e,this.cmd=o,this._hyperscript=self._hyperscript,this.cmdMap=[],this.bus=new EventTarget}r.prototype.break=function(t){return console.log("=== HDB///_hyperscript/debugger ==="),this.ui(),new Promise((e,o)=>{this.bus.addEventListener("continue",()=>{if(this.ctx!==t){for(var n in t)delete t[n];Object.assign(t,this.ctx)}delete globalThis.hdb,e(this.runtime.findNext(this.cmd,this.ctx))},{once:!0})})};r.prototype.continueExec=function(){this.bus.dispatchEvent(new Event("continue"))};r.prototype.stepOver=function(){if(!this.cmd)return this.continueExec();var t=this.cmd&&this.cmd.type==="breakpointCommand"?this.runtime.findNext(this.cmd,this.ctx):this.runtime.unifiedEval(this.cmd,this.ctx);if(t.type==="implicitReturn")return this.stepOut();if(t&&t.then instanceof Function)return t.then(e=>{this.cmd=e,this.bus.dispatchEvent(new Event("step")),this.logCommand()});t.halt_flag?this.bus.dispatchEvent(new Event("continue")):(this.cmd=t,this.bus.dispatchEvent(new Event("step")),this.logCommand())};r.prototype.stepOut=function(){if(!this.ctx.meta.caller)return this.continueExec();var t=this.ctx.meta.callingCommand,e=this.ctx.me;this.ctx=this.ctx.meta.caller,console.log("[hdb] stepping out into "+this.ctx.meta.feature.displayName),this.ctx.me instanceof Element&&this.ctx.me!==e&&console.log("[hdb] me: ",this.ctx.me),this.cmd=this.runtime.findNext(t,this.ctx),this.cmd=this.runtime.findNext(this.cmd,this.ctx),this.logCommand(),this.bus.dispatchEvent(new Event("step"))};r.prototype.skipTo=function(t){this.cmd=t.cmd,this.bus.dispatchEvent(new Event("skip"))};r.prototype.rewrite=function(t,e){console.log("##",t);let o=t.cmd.parent,n;for(n of o.children)if(n.next===t.cmd)break;let s=t.next,u=this._hyperscript.internals.tokenizer.tokenize(e),i=this._hyperscript.internals.createParser(u).requireElement("command");console.log(i),i.startToken=t.startToken,i.endToken=t.endToken,i.programSource=t.programSource,i.sourceFor=function(){return e},n.next=i,i.next=s,i.parent=o,this.bus.dispatchEvent(new Event("step"))};r.prototype.logCommand=function(){var t=this.cmd.sourceFor instanceof Function,e=t?this.cmd.sourceFor():"-- "+this.cmd.type;console.log("[hdb] current command: "+e)};r.prototype.traverse=function(t){let e=[];return(function o(n){if(e.push(n),"children"in n)for(let s of n.children)o(s)})(t),e};var h=`
<div class="hdb" _="
on load trigger update end
on step from hdb.bus trigger update end
on skip from hdb.bus trigger update end
on continue from hdb.bus log 'done' then remove me.getRootNode().host">
<script type="text/hyperscript">
def escapeHTML(unsafe)
js(unsafe) return unsafe
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/\\x22/g, """)
.replace(/\\x27/g, "'") end
return it
end
def makeCommandWidget(i)
get \`<span data-cmd=\${i}><button class=skip data-cmd=\${i}>⤷</button>\`
if hdb.EXPERIMENTAL
append \`<button class=rewrite data-cmd=\${i}>Rewrite</button></span>\`
end
return it
end
def renderCode
set hdb.cmdMap to []
set src to hdb.cmd.programSource
-- Find feature
set feat to hdb.cmd
repeat until no feat.parent or feat.isFeature set feat to feat.parent end
-- Traverse, finding starts
for cmd in hdb.traverse(feat)
if no cmd.startToken continue end
append {
index: cmd.startToken.start,
widget: makeCommandWidget(hdb.cmdMap.length),
cmd: cmd
} to hdb.cmdMap
end
set rv to src.slice(0, hdb.cmdMap[0].index)
for obj in hdb.cmdMap index i
if obj.cmd is hdb.cmd
append obj.widget + '<u class=current>' +
escapeHTML(src.slice(obj.index, hdb.cmdMap[i+1].index)) + '</u>' to rv
else
append obj.widget + escapeHTML(src.slice(obj.index, hdb.cmdMap[i+1].index)) to rv
end
end
return rv
end
def truncate(str, len)
if str.length <= len return str end
return str.slice(0, len) + '\u2026'
def prettyPrint(obj)
if obj is null return 'null' end
if Element.prototype.isPrototypeOf(obj)
set rv to '<<span class="token tagname">' +
obj.tagName.toLowerCase() + "</span>"
for attr in Array.from(obj.attributes)
if attr.specified
set rv to rv +
' <span class="token attr">' + attr.nodeName +
'</span>=<span class="token string">"' + truncate(attr.textContent, 10) +
'"</span>'
end
end
set rv to rv + '>'
return rv
else if obj.call
if obj.hyperfunc
get "def " + obj.hypername + ' ...'
else
get "function "+obj.name+"(...) {...}"
end
else if obj.toString
call obj.toString()
end
return escapeHTML((it or 'undefined').trim())
end
<\/script>
<header _="
on pointerdown(clientX, clientY)
halt the event
call event.stopPropagation()
get first .hdb
measure its x, y
set xoff to clientX - x
set yoff to clientY - y
repeat until event pointerup from document
wait for pointermove or pointerup from document
add {
left: \${its clientX - xoff}px;
top: \${its clientY - yoff}px;
} to .hdb
end
">
<h2 class="titlebar">HDB</h2>
<ul role="toolbar" class="toolbar" _="on pointerdown halt">
<li><button _="on click call hdb.continueExec()">
⏵ Continue
</button>
<li><button _="on click call hdb.stepOver()">
↷ Step Over
</button>
</ul>
</header>
<section class="sec-code">
<div class="code-container">
<pre class="code language-hyperscript" _="
on update from .hdb if hdb.cmd.programSource
put renderCode() into me
if Prism
call Prism.highlightAllUnder(me)
end
go to bottom of .current in me
end
on click
if target matches .skip
get (target's @data-cmd) as Int
call hdb.skipTo(hdb.cmdMap[result])
end
if target matches .rewrite
set cmdNo to (target's @data-cmd) as Int
set span to the first <span[data-cmd='\${cmdNo}'] />
put \`<form class=rewrite><input id=cmd></form>\` into the span
end
end
on submit
halt the event
get (closest @data-cmd to target) as Int
call hdb.rewrite(hdb.cmdMap[result], #cmd's value)
end
"><code></code></pre>
</div>
</section>
<section class="sec-console" _="
-- Print context at startup
init repeat for var in Object.keys(hdb.ctx) if var is not 'meta'
send hdbUI:consoleEntry(input: var, output: hdb.ctx[var]) to #console">
<ul id="console" role="list" _="
on hdbUI:consoleEntry(input, output)
if no hdb.consoleHistory set hdb.consoleHistory to [] end
push(input) on hdb.consoleHistory
set node to #tmpl-console-entry.content.cloneNode(true)
put the node at end of me
set entry to my lastElementChild
go to bottom of the entry
put escapeHTML(input) into .input in the entry
if no output
call hdb._hyperscript.parse(input)
if its execute is not undefined then call its execute(hdb.ctx)
else call its evaluate(hdb.ctx)
end
set output to it
end
put prettyPrint(output) as Fragment into .output in the entry
">
<template id="tmpl-console-entry">
<li class="console-entry">
<kbd><code class="input"></code></kbd>
<samp class="output"></samp>
</li>
</template>
</ul>
<form id="console-form" data-hist="0" _="on submit
send hdbUI:consoleEntry(input: #console-input's value) to #console
set #console-input's value to ''
set @data-hist to 0
set element oldContent to null
halt
on keydown[key is 'ArrowUp' or key is 'ArrowDown']
if no hdb.consoleHistory or exit end
if element oldContent is null set element oldContent to #console-input.value end
if event.key is 'ArrowUp' and hdb.consoleHistory.length > -@data-hist
decrement @data-hist
else if event.key is 'ArrowDown' and @data-hist < 0
increment @data-hist
end end
set #console-input.value to hdb.consoleHistory[hdb.consoleHistory.length + @data-hist as Int]
or oldContent
halt default
on input if @data-hist is '0' set element oldContent to #console-input.value">
<input id="console-input" placeholder="Enter an expression…"
autocomplete="off">
</form>
</section>
<style>
.hdb {
border: 1px solid #888;
border-radius: .3em;
box-shadow: 0 .2em .3em #0008;
position: fixed;
top: .5em; right: .5em;
width: min(40ch, calc(100% - 1em));
max-height: calc(100% - 1em);
background-color: white;
font-family: sans-serif;
opacity: .9;
z-index: 2147483647;
color: black;
display: flex;
flex-flow: column;
}
* {
box-sizing: border-box;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: .4em;
}
.titlebar {
margin: 0;
font-size: 1em;
touch-action: none;
}
.toolbar {
display: flex;
gap: .35em;
list-style: none;
padding-left: 0;
margin: 0;
}
.toolbar a, .toolbar button {
background: #2183ff;
border: 1px solid #3465a4;
box-shadow: 0 1px #b3c6ff inset, 0 .06em .06em #000;
border-radius: .2em;
font: inherit;
padding: .2em .3em;
color: white;
text-shadow: 0 1px black;
font-weight: bold;
}
.toolbar a:hover .toolbar a:focus, .toolbar button:hover, .toolbar button:focus {
background: #94c8ff;
}
.toolbar a:active, .toolbar button:active {
background: #3465a4;
}
.sec-code {
border-radius: .3em;
overflow: hidden;
box-shadow: 0 1px white inset, 0 .06em .06em #0008;
background: #bdf;
margin: 0 .4em;
border: 1px solid #3465a4;
}
.hdb h3 {
margin: 0;
font-size: 1em;
padding: .2em .4em 0 .4em;
}
.code-container {
display: grid;
line-height: 1.2em;
height: calc(12 * 1.2em);
border-radius: 0 0 .2em .2em;
overflow: auto;
scrollbar-width: thin;
scrollbar-color: #0003 transparent;
}
.code, #console, #console-input {
font-family: Consolas, "Andale Mono WT", "Andale Mono", "Lucida Console", "Lucida Sans Typewriter", "DejaVu Sans Mono", "Bitstream Vera Sans Mono", "Liberation Mono", "Nimbus Mono L", Monaco, "Courier New", Courier, monospace;
}
.code {
width: 0;
margin: 0;
padding-left: 1ch;
tab-size: 2;
-moz-tab-size: 2;
-o-tab-size: 2;
}
.current {
font-weight: bold;
background: #abf;
}
.skip {
padding: 0;
margin: 2px;
border: 1px solid #3465a4;
border-radius: 50%;
color: #3465a4;
background: none;
font-weight: bold;
font-size: 1.2em;
width: calc(2ch / 1.2 - 4px);
height: calc(2ch / 1.2 - 4px);
line-height: 0.6;
}
.skip:hover {
background: #3465a4;
color: #bdf;
}
#console {
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: #afc2db transparent;
height: calc(12 * 1.2em);
list-style: none;
padding-left: 0;
margin: 0 .4em .4em .4em;
position: relative;
word-wrap: break-word;
}
#console>*+* {
margin-top: .5em;
}
.console-entry>* {
display: block;
}
.console-entry .input { color: #3465a4; }
.console-entry .output { color: #333; }
.console-entry .input:before { content: '>> ' }
.console-entry .output:before { content: '<- ' }
#console-form {
margin: 0 .4em .4em .4em;
}
#console-input {
width: 100%;
font-size: inherit;
}
.token.tagname { font-weight: bold; }
.token.attr, .token.builtin, .token.italic { font-style: italic; }
.token.string { opacity: .8; }
.token.keyword { color: #3465a4; }
.token.bold, .token.punctuation, .token.operator { font-weight: bold; }
</style>
</div>
`;r.prototype.ui=function(){var t=document.createElement("div"),e=t.attachShadow({mode:"open"});t.style.cssText="all: initial",e.innerHTML=h,document.body.appendChild(t),this._hyperscript.process(e.querySelector(".hdb"))};function l(t){t.addCommand(a.keyword,a.parse.bind(a))}typeof self<"u"&&self._hyperscript&&self._hyperscript.use(l);export{l as default};