◆ XML から JavaScript 生成する

ふと HJML (Hyper Javascript Markup Language) という謎単語が頭に浮かびました
(HTML は Hyper Text Markup Language の略です)

なさそう……
だけど意外とありそう…… とググってみるとそれらしきものはありません

これは     作ってみるしかないかな!


ということで 作ってみました

とりあえず HTML ぽいものを書いて JavaScript に変換することができればおっけいでしょう(名前的に


構文考えて自分でパースしてどうこうするものはたまに作っていますが 意外と大変です
でも今回は XML が構文になっているのでパースが簡単です

あとはどういう構造で書いたらどんな JavaScript が出力されるかを決めて それを実装するだけです

最初は
<let a b=1>

で変数宣言 みたいなものを考えていて終了タグなしタグや バリューなしの属性あり にしたかったのですが DOMParser はあんまり柔軟な設定ができないです

XML としてパースするとキチッと決まった規格通りでこういうことができませんし HTML としてパースするとバリューなし属性は使えますが タグ名で構造の自動修正されるのや br タグみたいな終了タグ不要タグを自分で設定できません

そんな問題があって省略記法は諦めてちゃんとした XML として扱うことにしました

hjml

コードは最後にあります
先に使い方から

hjml.js をロードして
console.log(hjml.parse(xml_text))

これだけです

全体

変換すると
<function xmlns:j="hjml" xmlns:var="hjml" xmlns:fn="hjml" call-immediate="true">
<fn:console.log>
<string>
text
</string>
<number>
1e2
</number>
</fn:console.log>
</function>

(function (){
console.log("text", 1e2)
})()

になります

root element の
<function xmlns:j="hjml" xmlns:var="hjml" xmlns:fn="hjml" call-immediate="true">
</function>

は固定です

内側に好きに要素を入れていきます

関数呼び出し

上の例の console.log にあるように
<fn:console.log></fn:console.log>

の中に引数を入れます

fn 名前空間を使えば関数呼び出しです
自分で関数定義したものを呼び出すときも一緒です

また
<call name="function_name"></call>

と書いても呼び出し可能です


最初こっちだけだったのですが もっと楽したいと思って fn 名前空間ができました

<true/>
<false/>
<null/>
<undefined/>
<string>10</string>
<number>10</number>
<array>
    <true/>
    <false/>
    <boolean>true</boolean>
    <boolean>false</boolean>
</array>
<object>
    <keyname>
        <number>1</number>
    </keyname>
    <name>
        <string>1</string>
    </name>
</object>

こんな風に表現します

number 型や boolean 型で別れてますが 文字列とみなして 「"」 で囲むかどうかです
true や 1 とあった時に文字列か判断できないですし

配列は子要素が配列の要素として出力されます

オブジェクトは <object> タグの子要素はキー名になります
各キーの子要素が そのキーの値です

変数

宣言

<let a="100"/>
<const b="100" j:type="string"/>

let と const の 2 つが使えます
var による宣言はできません

var は変数参照につかいます

属性名がそのまま変数名になり 属性の value が初期値として代入される値となります
宣言だけで代入しない場合は value を空文字にします
XML なので省略不可です

属性を複数かけば まとめて宣言可能です

value がそのまま入るので文字列にしたい場合は j:type="string" という属性を指定します
これをつけるとすべて文字列になるのでまとめて宣言する場合は文字列とその他に分けないといけないです

配列や関数実行などをしたい場合は JavaScript コードそのままを value にすることもできます
<let a="[1]" b="{a:1}" c="document.querySelector(name)"/>

XML 風に書くには子要素を使います
<object></object> と同じように 子要素に名前 さらにその子要素に値を表す XML 要素を入れます
<const>
<obj>
<object>
<key1>
<string>&#x0020;1&#x00A0;</string>
</key1>
<key2>
<boolean>false</boolean>
</key2>
<arr>
<array>
<false/>
<true/>
<undefined/>
<number>10</number>
<null/>
<string>abc</string>
</array>
</arr>
</object>
</obj>
</const>


const obj = {
key1: "1 ",
key2: false,
arr: [
false,
true,
undefined,
10,
null,
"abc"
]
}

に変換されます
半角空白は無視されますので nbsp を入れて前後の空白を維持します

代入


let の場合は再代入可能です
const で宣言していても変換エラーにはなりませんが実行時にエラーになります

代入は <assign to="val"></assign> を使います
to 属性で変数名を設定して 子要素に代入する値を入れます

変数参照


変数の値を使うときに var を使います
<assign to="val" xmlns:var="hjml">
<call name="sum">
<var name="a"/>
<b/>
<var:x/>
</call>
</assign>
val = sum(a, b, x)

<var name="var_name"/> が基本ですが 直接 <var_name /> とタグで書くこともできます
しかし assign や let など決められた意味のあるタグ名は使えません

なので <var name="var_name"/> のほうが安全といえます
ここでも省略記法として <var:var_name/> と名前空間を使うこともできます

演算子

and などの演算子タグの子要素に対象を書きます
<and>
    <or>
        <true/>
        <false/>
    </or>
    <add>
        <number>-3</number>
        <number>2</number>
        <number>1</number>
    </add>
<and>
((true || false) && (-3 + 2 + 1))

演算子のネストがあると余分にカッコができてしまうのは対策が面倒なので仕様です
この問題のせいで演算子許可せず全部関数にしてしまおうかとも思ったのですが 演算子くらい使えたほうがいいかなと思ってやめました

使えるのは

add(+), and(&&), or(||), equals(===), lazy-equals(==)

だけです

拡張可能なので掛け算などは必要なら作ってください という扱いです

関数定義

function タグを使って 関数定義が可能です
<function j:name="plus" a="" b="1">
<return>
<add>
<var:a/>
<b/>
</add>
</return>
</function>
<function j:name="add_all" j:arguments="args">
<let total="0" j:type="number"/>
<for from="args" as="e">
<assign to="total">
<fn:plus>
<var name="total"/>
<var name="e"/>
</fn:plus>
</assign>
</for>
<return var="total"/>
</function>
function plus(a, b= 1){
return (a + b)
}
function add_all(...args){
let total = 0
for(const e of args){
total = plus(total, e)
}
return total
}

functon タグの j:name 属性で関数名を定義します
その他の属性は引数になります
j: 名前空間なしの name は name という引数になります
属性の value は初期値で 空文字は初期値なしです

call-immediate 属性があると (function(){})() のように即時実行されるようにカッコをつけます

return は <return> で var 属性で変数名指定するか子要素に値を書きます

分岐

if と stif があります

if では条件演算子(三項演算子) を使って式として分岐処理を出力します
stif では if 文を使って分岐処理を出力します

どちらも子要素に cond, then, else の 3 つのタグを書きます
順番は自由です
ただし cond が 2 つあるといったときは最初のものしか見ません
stif は else の省略が可能です

elseif はありません

cond の子要素には分岐条件となる値を then と else には cond の値が true または false のときの値を書きます
stif はブロックになるので複数の子要素を書くことができます
if は一つ目しか見ません
<if>
<cond>
<false/>
</cond>
<then>
<true/>
</then>
<else>
<call name="plus">
<val/>
<call name="plus1">
<var name="x"/>
</call>
</call>
</else>
</if>
<stif>
<cond>
<and>
<equals>
<var name="x"/>
<value>1</value>
</equals>
<lazy-equals>
<val/>
<value>301</value>
</lazy-equals>
</and>
</cond>
<then>
<return/>
</then>
</stif>
false
? true
: plus(val, plus1(x))
if((x === 1 && val == 301)){
return
}

こうなります

ループ

while と for タグが使えます

while は cond と do の子要素を持ち cond の条件が true のときに do の処理を行います

do は stif の then や else のようにブロックです
複数の子要素がかけます

for は for-of 専用です
from と as 属性を持ちます
for 自身がブロックで実行する内容の複数の子要素を持てます
<for from="args" as="e">
<assign to="total">
<fn:plus>
<var name="total"/>
<var name="e"/>
</fn:plus>
</assign>
</for>
for(const e of args){
total = plus(total, e)
}

拡張

HTML のカスタムエレメントのようにタグを定義できます
タグが行う処理を JavaScript で定義します
<register>
<sub argument="element">
<![CDATA[
return "(" + Array.from(element.children, hjml.prv.call).join(" - ") + ")"
]]>
</sub>
<div>
<![CDATA[
return "(" + Array.from(elem.children, hjml.prv.call).join(" / ") + ")"
]]>
</div>
</register>

register 要素の子要素に作成するタグ名を入れます
その中の CDATA セクションが実行する関数の body です

引数には elem という名前で要素自身が渡されます
その要素の子要素や属性を見て文字列で出力するコードを返します

elem という名前は argument 属性の値で変更可能です

もうひとつ hjml というオブジェクトが渡されます
tags という tag ごとの処理をまとめたオブジェクトとその他の関数をまとめた prv の 2 つが入っています
register タグでの登録はこの tags のプロパティに上書きするだけなので ここを直接書き換えて全体の動きを変更も可能です
tags を触れなくても register の動作だけでも既存のタグの動作を上書きしてしまうことは可能です


register で登録したタグはどこに書いていても関数が呼び出されるわけではありません
while タグや if タグの中は規定のタグ以外は無視されますし 一つ目の要素しか扱わないタグの中に 2 つ目で書いても呼び出されることはありません

基本はブロックの子要素か処理される式の中 (add の中や assign の 1 つ目の要素) に書きます

エラーは

HTML 感あるように極力エラーは起きないようにしてます
ただ簡単な確認しかしてないので100%起きない保証はないです

その分 生成された JavaScript が構文に沿っていなくて実行時エラーが出ることはよくあります

Demo

このページで試せます

DEMO

さいごに

見るからに手間が増えていて実用性のないものです
いわゆるネタ言語ってところでしょうか

なので途中で飽きてきて あってもいいのに無い機能も多々あります
それでも結構なコード量だと思います
(この長さなのにほとんどエラーなく動いたのはちょっと驚き)

JavaScript 感あるように出力する関数じゃなくて この XML → JavaScript の変換機能自体を拡張することもできるようになってます
既存関数も上書き可能です

なので 文字列エスケープ対応してなくて使いづらーい とか ** 演算子ほしいと思ったなら自分で書き換えることもできます
と言っても JavaScript なので実行時に拡張するんじゃなくてソース自体を書き換えればいいんですけどね

ソース

!function(){
var tags = {
// [stmt/exp]
// make function
function(elem){
var body = prv.block(elem)
var name = elem.getAttribute("j:name") || ""
var arguments_name = elem.getAttribute("j:arguments")
var args
if(arguments_name){
args = `...${arguments_name}`
}else{
args = Array.from(elem.attributes, attr => {
if(attr.prefix === null && !attr.name.includes("-")){
var value
if(attr.value === ""){
value = ""
}else{
try{
JSON.parse(attr.value)
value = `= ${attr.value}`
}catch(e){
value = `= "${attr.value}"`
}
}
return `${attr.localName}${value}`
}
}).filter(e => e).join(", ")
}
var fn = `function ${name}(${args}){\n${body.replace(/^/gm, "\t")}\n}`
return elem.hasAttribute("call-immediate")
? `(${fn})()`
: fn
},
// [exp]
lambda(elem){
if(elem.firstElementChild){
var arg = elem.getAttribute("argument")
var body = prv.call(elem.firstElementChild)
return `${arg} => ${body}`
}else{
return `() => {}`
}
},
// [exp]
call(elem){
var name = elem.getAttribute("name")
var args = Array.from(elem.children, prv.call).filter(e => e)
return `${name}(${args.join(", ")})`
},
// [stmt]
let(elem){
return prv.define(elem, "let")
},
// [stmt]
const(elem){
return prv.define(elem, "const")
},
// [exp]
value(elem){
var str = ""
Array.from(elem.childNodes).some(e => {
if(e.data) str += e.data
return e.nodeType !== Node.TEXT_NODE
})
try{
JSON.parse(str)
return str
}catch(err){
return `"${str}"`
}
},
// [exp]
number(elem){
if(elem.firstElementChild){
return `+${prv.call(elem.firstElementChild).trim()}`
}else{
return elem.innerHTML.trim()
}
},
// [exp]
string(elem){
if(elem.firstElementChild){
return `(${prv.call(elem.firstElementChild).trim()}).toString()`
}else{
// trim whitespace except nbsp
var str = elem.innerHTML.replace(/(^[ \t\n\t]+)|([ \t\n\r]+$)/g, "")
return `"${str}"`
}
},
// [exp]
boolean(elem){
if(elem.innerHTML.trim().toLowerCase() === "true"){
return "true"
}else if(elem.innerHTML.trim().toLowerCase() === "false"){
return "false"
}

return `!!(${prv.call(elem.firstElementChild).trim()})`
},
// [exp]
array(elem){
var items = Array.from(elem.children, prv.call).filter(e => e).join(",\n")
return "[\n" + items.replace(/^/gm, "\t") + "\n]"
},
// [exp]
object(elem){
var items = Array.from(elem.children, slem => {
var key = slem.localName
var vlem = slem.firstElementChild
var value = vlem && prv.call(vlem).trim()
return `${key}${vlem ? ": " + value : ""}`
}).join(",\n")
return "{\n" + items.replace(/^/gm, "\t") + "\n}"
},
// [exp]
true(elem){
return "true"
},
// [exp]
false(elem){
return "false"
},
// [exp]
null(elem){
return "null"
},
// [exp]
undefined(elem){
return "undefined"
},
// [exp]
var(elem){
return elem.getAttribute("name") || ""
},
// [exp]
if(elem){
var cond_elem = prv.childSearch(elem, "cond")
var cond_exp = cond_elem
? prv.call(cond_elem.firstElementChild)
: ""
var then_elem = prv.childSearch(elem, "then")
var then_exp = then_elem && then_elem.firstElementChild
? prv.call(then_elem.firstElementChild)
: ""
var else_elem = prv.childSearch(elem, "else")
var else_exp = else_elem && else_elem.firstElementChild
? prv.call(else_elem.firstElementChild)
: ""
return `${cond_exp}\n\t? ${then_exp}\n\t: ${else_exp}`
},
// [exp]
equals(elem){
return prv.equals(elem, "===")
},
// [exp]
"lazy-equals"(elem){
return prv.equals(elem, "==")
},
// [exp]
assign(elem){
var from = elem.getAttribute("from")
var to = elem.getAttribute("to")
if(to && !from){
var from_elem = elem.firstElementChild
? elem.firstElementChild.localName.toLowerCase() === "from"
? elem.firstElementChild
: elem
: null
from = from_elem.firstElementChild ? prv.call(from_elem.firstElementChild) : ""
}else{
var from_elem = prv.childSearch(elem, "from")
var to_elem = prv.childSearch(elem, "to")
from = from_elem.firstElementChild ? prv.call(from_elem.firstElementChild) : ""
to = to.firstElementChild ? prv.call(to_elem.firstElementChild) : ""
}
return `${to} = ${from}`
},
// [exp]
and(elem){
return `(${Array.from(elem.children, prv.call).filter(e => e).join(" && ")})`
},
// [exp]
or(elem){
return `(${Array.from(elem.children, prv.call).filter(e => e).join(" || ")})`
},
// [exp]
add(elem){
return `(${Array.from(elem.children, prv.call).filter(e => e).join(" + ")})`
},
// [stmt]
stif(elem){
var cond_elem = prv.childSearch(elem, "cond")
var cond_exp = cond_elem
? prv.call(cond_elem.firstElementChild)
: ""
var then_elem = prv.childSearch(elem, "then")
var then_block = then_elem && then_elem.firstElementChild
? prv.block(then_elem)
: ""
var else_elem = prv.childSearch(elem, "else")
var else_block = else_elem && else_elem.firstElementChild
? prv.block(else_elem)
: ""

if(else_block){
return `if(${cond_exp}){\n${then_block.replace(/^/gm, "\t")}\n} else {${else_block.replace(/^/gm, "\t")}\n}`
}else{
return `if(${cond_exp}){\n${then_block.replace(/^/gm, "\t")}\n}`
}
},
// [stmt]
return(elem){
var v = elem.getAttribute("var")
if(v){
return `return ${v}`
}

if(elem.firstElementChild){
return `return ${prv.call(elem.firstElementChild).trim()}`
}

return "return"
},
// [stmt]
for(elem){
var from = elem.getAttribute("from") || ""
var as = elem.getAttribute("as") || ""
var block = prv.block(elem)
return `for(const ${as} of ${from}){\n${block.replace(/^/gm, "\t")}\n}`
},
// [stmt]
while(elem){
var cond_elem = prv.childSearch(elem, "cond")
var cond_exp = cond_elem
? prv.call(cond_elem.firstElementChild)
: ""
var do_elem = prv.childSearch(elem, "do")
var do_block = do_elem && do_elem.firstElementChild
? prv.block(do_elem)
: ""
return `while(${cond_exp}){\n${do_block.replace(/^/gm, "\t")}\n}`
},
// [extend]
register(elem){
Array.from(elem.children).forEach(child => {
var name = child.localName
var arg_name = child.getAttribute("argument") || "elem"
var cdata = Array.from(child.childNodes).find(e => e.nodeType === Node.CDATA_SECTION_NODE)
if(!cdata) return

tags[name] = Function(arg_name, "hjml", cdata.data)
})
},
}

var prv = {
// let, const
define(elem, def_type){
var type = (elem.getAttribute("j:type") || "").toLowerCase()
var attr_lets = Array.from(elem.attributes, attr => {
if(attr.prefix || attr.localName.includes("-")) return null
if(type !== "string" && attr.value === ""){
return `${def_type} ${attr.localName}`
}else{
return `${def_type} ${attr.localName} = ${type==="string"?'"':""}${attr.value}${type==="string"?'"':""}`
}
}).filter(e => e)
var children_lets = Array.from(elem.children, slem => {
var name = slem.localName
if(slem.firstElementChild){
var exp = prv.call(slem.firstElementChild)
return `${def_type} ${name} = ${exp.trim()}`
}else{
return `${def_type} ${name}`
}
})
return attr_lets.concat(children_lets).join("\n")
},
// equals
equals(elem, eq_type){
var items = Array.from(elem.children, prv.call)
return items.reduce((a, b) => {
a.prev && a.result.push(`${a.prev} ${eq_type} ${b}`)
a.prev = b
return a
}, {prev: null, result: []}).result.join(" && ")
},
// reference variable of elem name
refVar(elem){
return elem.localName
},
// call tagname's action
call(elem){
switch(elem.prefix){
case "var":
return prv.refVar(elem)
case "fn":
var args = Array.from(elem.children, prv.call)
return `${elem.localName}(${args.join(", ")})`
}
var fn = tags[elem.localName.toLowerCase()] || prv.refVar
return fn(elem, {tags, prv})
},
block(elem){
return Array.from(elem.children, child => {
var stmt = prv.call(child)

if(stmt && ["[", "(", "/", "`"].includes(stmt.codePointAt(0))){
stmt = ";" + stmt
}
return stmt
}).filter(e => e).join("\n")
},
childSearch(elem, tagname){
return Array.prototype.find.call(elem.children, e => e.localName.toLowerCase() === tagname)
},
}

window.hjml = {
parse(xml){
var root = new DOMParser().parseFromString(xml, "text/xml").firstElementChild
return prv.call(root)
},
}
}()

Gist