2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JPHACKS 2020Advent Calendar 2020

Day 9

ハッカソンで【vue.jsによるvue.jsのためのオンラインジャッジサービス】を開発した

Last updated at Posted at 2020-12-06

#前書き
先日jphacks2020に参加して、
frontEngine】というサービスを開発しました。
ファイナリストまで行き、その後あまり良い結果にならず、
ハッカソンとしての反省点などもありますが、
今回に関しては、frontEngineで実装された機能がどのように動いているのかを解説していきます。

今回のものを作成して感じたことは、ジャッジシステムを搭載するよりも、
userからのvue.jsのコードを受け取って、(セキュリティ上)安全にvue.jsを表示できる様になる等プログラミング教材として展開していくと、良いのではないかと考えております。

また、リアクションがどの程度もらえるかわかっていないので、
リアクションがあれば、どんどん記事を精錬させていく所存です。(質問もガンガンください!!)

知らない人もたくさんいると思うので、どういう機能があるかといいますと、
プロジェクト単位(router設定やページ登録)で表現できる!!
デザイン審査もできる(progateみたいにCSSをみているんじゃなくて、CSSの要素 && 要素間の位置をみている)!!
userが書いたコードをそのままプレビューすることができる!!
ジャッジシステムで採点してくれる!!
レーティングが反映される!!(この記事では解説しません)
#概要
今回解説する機能群

frontEngineのデモ

Vue.jsのTemplate部分解析
に挟まれている部分に対して、
こちら側で処理しやすいように解析します。

Vue.jsのscript部分解析

に挟まれている部分に対して、

jsのように自由に実行できる様に行います。
(ここの部分に)

Vue.jsのstyle部分解析

に挟まれている部分に対して、

dom側に適用しやすいようにします

上記三つを組み合わせて、vue.js単一ファイルとして機能させる
v-forだったり、v-ifだったりを機能させ、出力できる様にします。

プレビュー機能
$mount等で機能させます。

プロジェクト単位拡張
現状単一ファイルでしか、読み取れない問題をrouter/index.jsや各ページ単位の問題のように拡張します。

ジャッジシステム/スタイルジャッジシステム
正直ここが甘い作りになっていましたが、できる限りやったものを載せます

他にもターミナル機能やレーティングシステムなどもありますが、
それらに関しては私以外のチームメンバーが頑張ってくれたので、チームメンバーが記事を書いてくだされば、リンクを載せます。

##Vue.jsのTemplate部分解析
主にここでソースコードがあります。
domProcess
まず、説明する前に
hanoi
を任意の問題のホームのソースコード入力欄に入れてみて、どのような加工がされるのかをみていきます。
実際に入力すると

hanoi
{
  "open": true,
  "close": false,
  "name": "div",
  "others": [
    {
      "left": "class",
      "right": "exam4",
      "directive": false,
      "type": "variable",
      "variableType": "String",
      "variables": [
        "exam4"
      ]
    },
    {
      "left": "class",
      "right": "exam4",
      "directive": false,
      "type": "variable",
      "variableType": "String",
      "variables": [
        "exam4"
      ]
    }
  ],
  "class": {
    "left": "class",
    "right": "exam4",
    "directive": false,
    "type": "variable",
    "variableType": "String",
    "variables": [
      "exam4"
    ]
  },
  "unique": 0,
  "depth": 0,
  "children": [
    {
      "open": true,
      "close": false,
      "name": "answer-card",
      "others": [
        {
          "left": "class",
          "right": "hanoi",
          "directive": false,
          "type": "variable",
          "variableType": "String",
          "variables": [
            "hanoi"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('left')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'left'"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('left')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'left'"
          ]
        }
      ],
      "class": {
        "left": "class",
        "right": "hanoi",
        "directive": false,
        "type": "variable",
        "variableType": "String",
        "variables": [
          "hanoi"
        ]
      },
      "parentId": 0,
      "unique": 1,
      "depth": 1,
      "children": [
        {
          "open": true,
          "close": false,
          "name": "answer-card",
          "others": [
            {
              "left": "v-for",
              "right": "left",
              "directive": true,
              "target": {
                "value": "value",
                "index": "index"
              },
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "@click",
              "right": "trans(value, 'left')",
              "directive": false,
              "type": "function",
              "functionTarget": "trans",
              "functionArgument": [
                "value",
                " 'left'"
              ]
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            }
          ],
          "v-for": {
            "left": "v-for",
            "right": "left",
            "directive": true,
            "target": {
              "value": "value",
              "index": "index"
            },
            "type": "variable",
            "variableType": "global"
          },
          "parentId": 1,
          "unique": 2,
          "depth": 2,
          "children": [
            {
              "value": "{{value}}",
              "reserves": [
                {
                  "start": 0,
                  "end": 8,
                  "text": "value",
                  "textRawValue": "value",
                  "type": "variable",
                  "variableType": "global"
                }
              ],
              "parentId": 2,
              "unique": 3,
              "depth": 3,
              "name": "reserveText"
            }
          ]
        }
      ]
    },
    {
      "open": true,
      "close": false,
      "name": "answer-card",
      "others": [
        {
          "left": "class",
          "right": "hanoi",
          "directive": false,
          "type": "variable",
          "variableType": "String",
          "variables": [
            "hanoi"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('center')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'center'"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('center')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'center'"
          ]
        }
      ],
      "class": {
        "left": "class",
        "right": "hanoi",
        "directive": false,
        "type": "variable",
        "variableType": "String",
        "variables": [
          "hanoi"
        ]
      },
      "parentId": 0,
      "unique": 6,
      "depth": 1,
      "children": [
        {
          "open": true,
          "close": false,
          "name": "answer-card",
          "others": [
            {
              "left": "v-for",
              "right": "center",
              "directive": true,
              "target": {
                "value": "value",
                "index": "index"
              },
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "@click",
              "right": "trans(value, 'center')",
              "directive": false,
              "type": "function",
              "functionTarget": "trans",
              "functionArgument": [
                "value",
                " 'center'"
              ]
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            }
          ],
          "v-for": {
            "left": "v-for",
            "right": "center",
            "directive": true,
            "target": {
              "value": "value",
              "index": "index"
            },
            "type": "variable",
            "variableType": "global"
          },
          "parentId": 6,
          "unique": 7,
          "depth": 2,
          "children": [
            {
              "value": "{{value}}",
              "reserves": [
                {
                  "start": 0,
                  "end": 8,
                  "text": "value",
                  "textRawValue": "value",
                  "type": "variable",
                  "variableType": "global"
                }
              ],
              "parentId": 7,
              "unique": 8,
              "depth": 3,
              "name": "reserveText"
            }
          ]
        }
      ]
    },
    {
      "open": true,
      "close": false,
      "name": "answer-card",
      "others": [
        {
          "left": "class",
          "right": "hanoi",
          "directive": false,
          "type": "variable",
          "variableType": "String",
          "variables": [
            "hanoi"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('right')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'right'"
          ]
        },
        {
          "left": "@click",
          "right": "utusu('right')",
          "directive": false,
          "type": "function",
          "functionTarget": "utusu",
          "functionArgument": [
            "'right'"
          ]
        }
      ],
      "class": {
        "left": "class",
        "right": "hanoi",
        "directive": false,
        "type": "variable",
        "variableType": "String",
        "variables": [
          "hanoi"
        ]
      },
      "parentId": 0,
      "unique": 11,
      "depth": 1,
      "children": [
        {
          "open": true,
          "close": false,
          "name": "answer-card",
          "others": [
            {
              "left": "v-for",
              "right": "right",
              "directive": true,
              "target": {
                "value": "value",
                "index": "index"
              },
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "@click",
              "right": "trans(value, 'right')",
              "directive": false,
              "type": "function",
              "functionTarget": "trans",
              "functionArgument": [
                "value",
                " 'right'"
              ]
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            },
            {
              "left": "key",
              "right": "index",
              "directive": true,
              "type": "variable",
              "variableType": "global"
            }
          ],
          "v-for": {
            "left": "v-for",
            "right": "right",
            "directive": true,
            "target": {
              "value": "value",
              "index": "index"
            },
            "type": "variable",
            "variableType": "global"
          },
          "parentId": 11,
          "unique": 12,
          "depth": 2,
          "children": [
            {
              "value": "{{value}}",
              "reserves": [
                {
                  "start": 0,
                  "end": 8,
                  "text": "value",
                  "textRawValue": "value",
                  "type": "variable",
                  "variableType": "global"
                }
              ],
              "parentId": 12,
              "unique": 13,
              "depth": 3,
              "name": "reserveText"
            }
          ]
        }
      ]
    }
  ]
}

このようになりました。
みてわかる通り、AST分析を行なっております。

ここで説明のために、
一つのモノ(open,close,name,others,children等を持っている一要素)に対して、
部品と呼ぶことにします。

基本的にゴリゴリっと'<'とか'/>'等を検知して、
BFSやらDFSを行なっております。
Text要素か、dom要素で大きくパターンが変わるのですが、
まず、dom要素からみていきます。
dom要素では、基本的に、':key='だったり'key='だったり'active'等の三パターンに分かれます。
それらの要素をotherとして各要素のothersに格納します。
(=を基準としてみて、左側の属性をleft,右側の属性をrightとしています。)
それに伴い、:であれば、directiveとし、それ以外は文字列型格納として処理します。
(同様に=でない要素に関してはtrueとして格納します)
また、functionかそうでないかは()があるかどうかで、判断できるので、functionであれば、
argumentも別枠として格納しておきます。

次にdom要素として、v-forは、記法が特殊で、left,rightで格納する以外に、前処理してないと扱えたものではないので、ofもしくはinの右側を、通常のrightの部分に格納し、変数部分((value, index) in items のitems,index部分)に関しては、targetというところに格納しておきます。

次にText要素ですが、'{{}}'か生のtextのどちからに分かれると思います。
ただ、もし、{{}}や生のTextが混同しているとしても、dom要素に触れるまで('<name ~...'を見つけるまでは)は、同じ部品として対処していきます。
その部品の中で、あらたにreservesを定義し、またnameを'reserveText'とします。
reservesの中では、{{}}の要素か生のTextかを区別します。
{{で始まれば,}}があるまでは、同一の要素とみなし、生のTextは{{が始まるまでは、生のTextとして処理します。
(startやendなども一応記録として載せていますが、type要素以外domPropery.jsがよしなにしてくれるようにしてます。)

##Vue.jsのscript部分解析
主に下記3つがメインファイルになります
moduleProcess
utility
execScript
script解析に関しては、汎用性が高いと思いますので、npmライブラリ化しようかと考えております。
(公開しました。
https://github.com/imamiya-masaki/engine-js-test
https://www.npmjs.com/package/exec-engine-js)

ので詳しい処理の流れよりもざっくりどのようになっているかを解説致します。
まず、MainProcessから<script></script>内部がmoduleProcessに渡されます。
最初に、module単位でBabelを使いAST化し、各ブロック(data,props,methods,computedなど)に渡され、各々のモノが実行されます。
babelを使ってもAST化されるだけで実行や、変数の変換などは行えません。(仮に行えてもuserからの入力jsをそのまま実行できたらセキュリティ上やばいですよね)
そこで今回は、javascriptの上で動くjavascriptに仕様等が似ている言語のインタプリタを作成していきます。

今回では、propsは別処理としておいているので、dataからみていくことにします。
###data
dataProcess実際にdataProcessの中をみていきます。

dataProcess.js
import { CheckProperty, getProperty } from './utility.js'
import { global } from '../moduleProcess.js'
export default function (body) {
  const output = {}
  for (const property of body.body.body[0].argument.properties) {
    const getter = getProperty(property)
    output[property.key.name] = getter
  }
  return output
}

引数として、babelでAST化されたdataの中身が渡され、それをfor文で回していきます。
ここで**getProperty**という超便利funcが呼び出されてますが、これは渡されたものを許可された(こちら側で設定した)範囲でjsの値として返してくれます。(実際にconsoleなどは定義してないので、呼び出されることはありません)
key名: value
というdata型で定義されているものを、outputというオブジェクトに定義されているkey名をkeyとして、getPropertyを使いASTから正常な値となったものを該当するkeyのvalueとして格納し、moduleに返します。

###methods
次に、methodsProcessをみていきます。

methodsProcess.js
export default function (body) {
  const output = {}
  for (const property of body.value.properties) {
    output[property.key.name] = property.value
    if (output[property.key.name]) {
      output[property.key.name].func = true
    }
  }
  return output
}

methodsをみていくと、dataに比べgetPropertyのような、ASTを扱える値に変換するような物がなく、
直接ASTを代入しています。
実際に,上記で使ったhanoiのファイルを流し込んでやると、
hanoiが持っているtransとutusuがoutputに格納されるはずなのでみてみます。

hanoiのmethods
{trans: Node, utusu: Node}
trans: Node {type: "FunctionExpression", start: 356, end: 749, loc: SourceLocation, range: undefined, }
utusu: Node {type: "FunctionExpression", start: 762, end: 927, loc: SourceLocation, range: undefined, }
__proto__: Object

となり、試しに展開してみると、

hanoiのmethodsをjson展開

{
  "trans": {
    "type": "FunctionExpression",
    "start": 356,
    "end": 749,
    "loc": {
      "start": {
        "line": 20,
        "column": 11
      },
      "end": {
        "line": 31,
        "column": 5
      }
    },
    "id": null,
    "generator": false,
    "async": false,
    "params": [
      {
        "type": "Identifier",
        "start": 366,
        "end": 371,
        "loc": {
          "start": {
            "line": 20,
            "column": 21
          },
          "end": {
            "line": 20,
            "column": 26
          },
          "identifierName": "value"
        },
        "name": "value"
      },
      {
        "type": "Identifier",
        "start": 373,
        "end": 379,
        "loc": {
          "start": {
            "line": 20,
            "column": 28
          },
          "end": {
            "line": 20,
            "column": 34
          },
          "identifierName": "houkou"
        },
        "name": "houkou"
      }
    ],
    "body": {
      "type": "BlockStatement",
      "start": 381,

      ...省略...

      "directives": []
    },
    "func": true
  },
  "utusu": {
    "type": "FunctionExpression",
    "start": 762,
    "end": 927,
    "loc": {
      "start": {
        "line": 32,
        "column": 11
       
        ...省略...
     
                  },
                  "right": {
                    "type": "BooleanLiteral",
                    "start": 908,
                    "end": 913,
                    "loc": {
                      "start": {
                        "line": 35,
                        "column": 26
                      },
                      "end": {
                        "line": 35,
                        "column": 31
                      }
                    },
                    "value": false
                  }
                }
              }
            ],
            "directives": []
          },
          "alternate": null
        }
      ],
      "directives": []
    },
    "func": true
  }
}


と、まともに表現することができないほど(したらページが大変なことになってしまう)AST化が完全にされているファイルを格納しています。
methodsが呼ばれるタイミングを考えてみると、DOMで呼ばれるか、もしくはscript内で、
this参照で呼び出しにいく時だと思いますので、その時に便利メソッドgetScriptを呼び出すことで、実行するという仕様にします。

###getScript/execScript

次に、便利メソッドのスタメンであるgetScript/execScriptを紹介します。

実際の中身の詳しい解説に関しては要望があれば致しますが、今回はとりあえずどの様な場面で使われて、どの様な仕組みなのかについて解説致します。
基本的にはgetScriptを使うことにより、他funcからの呼び出しを違和感なく済ませることができます。

getScript
function getScript (body, array, preLocal) {
  return execScript(body, array, preLocal).returnArguments
}

また、execScriptが処理できる領域は

function
function (a,b,c) {
 //中身
}

このようなブロック単位が存在するfunctionを上手く処理することができます。

さて、次に引数に注目すると、body,array,preLocalという3つを受け取っていることがわかります。

bodyはmethodsがそのままのASTを保存していたように、bodyではmethodsに格納した様なASTを受け取ります。
arrayはfunctionの引数を配列型で受け取り、body.params(上記で示した例のa,b,cの部分)をkeyとして、このブロック単位のfunctionが持つローカル変数として格納します

array格納
let local = {}
if (array && body.params) {
    for (let i = 0; i < body.params.length; i++) {
      local[body.params[i].name] = array[i]
    }
  }

preLocalは、親block単位要素を引継ぎます。
これがどういうことかと言うと、私が実装しているif文の処理を参考に解説致します。

IfStatement
case 'IfStatement':
        let targetGO = access.body[i]
        let targetDo = null
        while (true) {
          if (!targetGO.hasOwnProperty('test')) {
            // else
            // -> つまりifでないものが全てとおる
            targetDo = targetGO
            break
          }
          let resultBool = isBool(targetGO.test, local)
          if (!resultBool) {
            // false
            if (targetGO.alternate) {
              targetGO = targetGO.alternate
            } else {
              break
            }
          } else {
            // true
            targetDo = targetGO.consequent
            break
          }
        }
        if (targetDo) {
          let get = execScript(targetDo, array, local)

          Object.keys(get.returnLocal || {}).forEach(key => {
            local[key] = get.returnLocal[key]
          })
        }
        break

この処理では、 if(targetDo){}の中身が、実行されうるif文のblockと考えてください。
そうすると、我々は一番上のblock要素のfunctionの中にいますが、if文の中の結果の変数などは、if文から出ると、棄却されなければいけません。そこを考えると、if文や、for文やwhile文などのblock要素も、一つ下のexecScriptで処理すべき領域と考えます。
そうすると、一つ下の階層(functionから考えて、if文やfor文等)は上の階層の変数などを変更しうる可能性があるので、preLocalの場所に自分のlocal情報を渡し

execScript
let local = {}
  if (preLocal) {
    local = Object.assign(local, preLocal)
  }

親のlocalの情報を参照できる様にします。
次に、break文などで終了する場合や単純に、処理が末端まで行えた場合、また親側の処理に戻りますので、

ifStatement
Object.keys(get.returnLocal || {}).forEach(key => {
            local[key] = get.returnLocal[key]
          })

とし、親側で親側で渡したlocalに対して、更新をかけます。
この更新を行うことにより、子の変数の棄却や、その他の子block内の処理をスムーズに行えます。

これら以外にも、
for文の実装などは変数の都度更新など行っておりますので、よかったらみてくださると嬉しいです。
(isBoolやcalculationやその他の処理もみてくれると嬉しいです。)

forStatement
case 'ForStatement':

        const target = access.body[i]
        const initTarget = target.init.declarations[0]
        const initName = initTarget.id.name
        let initIndex = calculation(initTarget.init, local)
        const readyupdate = target.update
        let updateCalculation = target.update.right
        if (!target.update.right) {
          // argument?
          updateCalculation = target.update
        }
        let updateName = ''
        if (readyupdate.left) {
          updateName = readyupdate.left.name
        } else {
          // argument
          updateName = readyupdate.argument.name
        }
        const readyBool = target.test
        while (isBool(readyBool, { ...local, [initName]: initIndex })) {
          let get = execScript(target.body, array, { ...local, [initName]: initIndex })
          Object.keys(get.returnLocal || {}).forEach(key => {
            if (key !== initName) {
              local[key] = get.returnLocal[key]
            }
          })
          if (get.returnOrder === 'break') {
            break
          }
          // updateFunc
          if (initName === updateName) {
            initIndex = calculation(updateCalculation, { ...local, [initName]: initIndex })
            if (initIndex === false) {
              console.error('why false!?', updateCalculation, initIndex)
              break
            }
          } else {
            local[updateName] = calculation(updateCalculation, { ...local, [initName]: initIndex })
          }
        }
        break

###getProperty/CheckProperty
便利メソッド二つ目のgetProperty/CheckPropertyです。

CheckPropertyは最初の方に作成して、のちにgetPropertyと併用する形となっているので、仕様上少しの違いが生じています(例えば、後述するglobalを自分の子供以降をみてglobalを使っていたり...)
CheckPropertyは、Object,Array,Int,String,Boolなどに対して、値として返してくれるfuncです。
一方getPropertyは、呼び出された場所にあるlocal(上述したfunctionのlocalだったり)や、オブジェクトのキーにアクセスしたり、こちら側で登録したモノ(今回で言えば、ObjectやNumberなど)にアクセスしたり、functionだったらgetScriptとして実行したり、global(vue.js単一ファイル上に存在するthisでアクセスできる場所)を扱ってそうでなければCheckpropertyを呼び出すfunctionです。

###module
もう一度moduleProcessをみてみます。
methodsや、dataやcomputedなどが、先ほど少し話したglobalに格納されていきます。
this参照した時や、domからのアクセスの時にgetPropertyを通して、globalから取り出すことで最適なfunctionだったり、パラメータを取得することができます。

##Vue.jsのstyle部分解析
styleProcess
Vue.jsのstyleとはここでは、vue.jsの<style scpoed></style>内部を指すこととします。
(やっていることは特殊なことはないので、特定ファイルのcssファイルなどにも応用できるかと思います。)

さてここで、vue.jsにはv-bind:styleというものがあります。
これを使えば、後述するプレビュー機能でも、cssに対して特別な設定をすることなく、オブジェクトを渡したら描画することができそうです。

styleProcess.js
 if (val == ' ' || val == '\n' || val == '/s' || val == '') {
      continue
    }
    if (val === '{') {
      let targetInput = 'tag'
      switch (target[0]) {
        case '.':
          targetInput = 'class'
          target.shift()
          break
        case '#':
          targetInput = 'id'
          target.shift()
          break
      }
      let targetVal = target.join('')
      output[targetInput][targetVal] = {}
      i++
      let outputKey = []
      let value = []
      let coron = false
      for (;i < style.length; i++) {
        if (style[i] == '}') {
          // とりあえずclassの階層構造は無視
          break
        }
        if (style[i] != ';') {
          if (style[i] == ':') {
            coron = true
            continue
          }
          if (!coron) {
            if (style[i] == ' ' || style[i] == '\n' || style[i] == '/s' || style[i] == '') {
              continue
            }
            outputKey.push(style[i])
          } else {
            value.push(style[i])
          }
        } else {
          output[targetInput][targetVal][outputKey.join('')] = value.join('')
          outputKey = []
          value = []
          coron = false
        }
      }
      target = []
    } else {
      target.push(val)
    }

短いので掲載しましたが、domProcessの簡略版の様な、
文字列を探索し、オブジェクトに直していきます。

##上記三つを組み合わせて、vue.js単一ファイルとして機能させる
domPreviewParse
MainProcess

ジャッジシステム、プレビュー機能と少し被りますが、
ここでは、プレビュー画面に載せても機能する用に一度ジャッジシステム -> domへのparseを紹介していきます。

まず、最初にジャッジシステムを通さず、templateをAST化したものを読み込むpureDomPreviewParseをみていきます。

pureDomPreviewParse.js
function pureDomPreviewParse (domTree, fileName) {
  console.log('getDomPreviewParse', domTree)
  const output = []
  let stack = [domTree]
  const parentParam = {}
  output.push('<div id="previewDOM">')
  while (stack.length > 0) {
    const take = stack.pop()
    const parseDom = {}
    const yoyaku = {}
    if (take.hasOwnProperty('class')) {
      let targetDom = []
      targetDom.push('\'' + fileName + '\'')
      if (take.class.hasOwnProperty('variables')) {
        take.class.variables.forEach(key => {
          targetDom.push('\'' + key + '\'')
        })
      }
      parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')'
    }
    if (take.hasOwnProperty('v-for')) {
      const targetValue = []
      if (take['v-for'].target.hasOwnProperty('value')) {
        targetValue.push(take['v-for'].target.value)
        yoyaku[take['v-for'].target.value] = 'value'
      }
      if (take['v-for'].target.hasOwnProperty('index')) {
        targetValue.push(take['v-for'].target.index)
        yoyaku[take['v-for'].target.index] = 'index'
      }
      let targetInput = ''
      const targetDom = []
      if (take['v-for'].type) {
        targetDom.push('\'' + take['v-for'].right + '\'')
        targetDom.push('\'' + fileName + '\'')
        Object.keys(parentParam).forEach(key => {
          targetDom.push('{' + key + ': ' + key + '}')
        })
        targetInput = 'this.domEvent(' + targetDom.join(',') + ')'
      }
      // const targetOutput = '(' + targetValue.join(',') + ')' + ' of ' +
      parseDom['v-for'] = '(' + targetValue.join(',') + ') of ' + targetInput
    }
    if (take.hasOwnProperty('others')) {
      // 現状v-forとclassを分けたらいけるか...?
      for (let i = 0; i < take.others.length; i++) {
        if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') {
          continue
        }
        let key = take.others[i].left
        if (take.others[i].directive) {
          key = ':' + key
        }
        let targetInput = ''
        const targetDom = []
        if (take.others[i].type) {
          let otherRight = take.others[i].right
          otherRight = otherRight.replace(/\'/g, '\\\'')
          targetDom.push('\'' + otherRight + '\'')
          targetDom.push('\'' + fileName + '\'')
          // targetDom.push(Object.keys(parentParam))
          Object.keys(parentParam).forEach(key => {
            targetDom.push('{' + key + ': ' + key + '}')
          })
          targetInput = 'domEvent(' + targetDom.join(',') + ')'
        }
        parseDom[key] = targetInput
      }
    }
    if (take.name === 'reserveText' && take.reserves) {
      const textOutput = []
      for (let i = 0; i < take.reserves.length; i++) {
        const reserveVal = take.reserves[i]
        if (reserveVal.type === 'direct') {
          textOutput.push(reserveVal.textRawValue)
        } else {
          const targetDom = []
          targetDom.push('\'' + reserveVal.textRawValue + '\'')
          targetDom.push('\'' + fileName + '\'')
          // targetDom.push(Object.keys(parentParam))
          Object.keys(parentParam).forEach(key => {
            targetDom.push('{' + key + ': ' + key + '}')
          })
          textOutput.push('{{ domEvent(' + targetDom.join(',') + ') }}')
        }
      }
      parseDom.reserveText = textOutput.join('')
    }
    // --各々の作用
    if (!parseDom.hasOwnProperty('reserveText')) {
      const pushOutput = []
      for (let i = 0; i < Object.keys(parseDom).length; i++) {
        const key = Object.keys(parseDom)[i]
        pushOutput.push(key + '="' + parseDom[key] + '"')
      }
      let endBlock = '>'
      if (!take.open && !take.enClose) {
        endBlock = '/>'
      }
      let startBlock = '<'
      if (take.enClose) {
        startBlock = '</'
      }
      if (take.closeParams && take.closeParams.length > 0) {
        take.closeParams.forEach(key => {
          delete parentParam[key]
        })
      }
      output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock)
    } else {
      output.push(parseDom.reserveText)
    }
    // --parseしてpush

    if (take.open) {
      const enCloseTag = {}
      enCloseTag.name = take.name
      enCloseTag.enClose = true
      stack.push(enCloseTag)
      if (Object.keys(yoyaku).length > 0) {
        enCloseTag.closeParams = []
        Object.keys(yoyaku).forEach(key => {
          parentParam[key] = yoyaku[key]
          enCloseTag.closeParams.push(key)
        })
      }
      if (take.hasOwnProperty('children')) {
        for (let i = take.children.length - 1; i >= 0; i--) {
          const childVal = take.children[i]
          stack.push(childVal)
        }
      }
    }
    // --子供に対する作用
    // --whileEnd
  }
  output.push('</div>')
  return output.join('')
}

基本的には、userがコーディングした、templateをそのままparseしなおせば良いのですが、
scriptだったり、global(style解析)にアクセスするようにしないといけません。
そのため、各々がもっている@clickや、v-for、classなどの対象としている変数をアクセスできるように、
domEventclassEventなどに変更する(他にもparseEventや、routerEventなどがあります)ことで、のちにプレビューする時に、$vueに、

PreviewField.vue
domEvent: function (order, path, userAction, ...arg) {
      let toParam = Object.assign({}, global)
      arg.forEach(x => {
        toParam = Object.assign(toParam, x)
      })
      const domPro = domProperty(order, toParam)
      if (userAction) {
        this.outputDom = domPreviewParse(saveDomTree, path)
        this.previewParse()
      }
      return domPro
    },
    classEvent: function (path, ...orders) {
      // class名を受け取る
      let outputObj = {}
      orders.forEach(key => {
        outputObj = Object.assign(outputObj, globalStyle[path].class[key])
      })
      return outputObj
    },
    parseEvent: function (param) {
      const splits = param.split('[')
      const gets = splits[0]
      const index = Number(splits[1].split(']')[0])
      if (global[gets]) {
        return global[gets][index]
      } else {
        return false
      }
    },
    routerEvent: function (param) {
      this.$emit('router-change', param)
    },

などをわたすことで、globalと、template部分との疎通を測ります。
(userActionなどはこの後後述します。)
実際にこれをプレビュー機能に載せようとすると、一つ問題があります。
基本的には、レンダリングやglobalなどの疎通はできるのですが、v-forの内部に@clickなどを設定すると、
与えられている要素(例えば配列をv-forで回していて、その配列の中の一つの要素をclickイベントに一つずつ与えてる処理)に対しては、v-forの内部の要素全てクリックしても、最後の要素(配列の最後)のclickeventが発火されます。(詳しい原因はわかってないのですが、参照渡しでdomが渡されていると仮定すれば、最後の要素が参照渡し的に代入されたとすれば、全ての要素が最後の要素になるのは仕方ないのかなと思います (:keyなどありますしね...))

そこで、v-for,v-ifやtemplateの動的レンダリングをこちら側で処理することを考えます。

ここで、MainProcessをみてみます。

MainProcess.js
  while (targets.length > 0) {
    const ifBool = true
    const getTar = targets.pop()
    const tar = Object.assign({}, getTar)
    if (tar.params) {
      // console.lo('targets!!', tar.params, tar)
    }
    tooru = false
    // -- v-for
    if (tar['v-for']) {
      const target = tar['v-for']
      if (target.type === 'variable' || target.type === 'function') {
        let data = domProperty(target.right, tar.params)
        // とりあえずdataはArray想定 本来ではObjectも考えないといけないよ
        if (Array.isArray(data)) {
          for (let i = data.length - 1; i >= 0; i--) {
            // tar.params = {}
            let nextTarget = Object.assign({}, tar)
            const params = {}
            const textParams = {}
            const keys = Object.values(target.target)
            params[keys[0]] = data[i]
            textParams[keys[0]] = String(keys[0] + '[' + i + ']')
            console.log('chh', data[i], data, target)
            if (keys.length === 2) {
              params[keys[1]] = i
              textParams[keys[1]] = i
            }
            nextTarget.paramIndex = i
            nextTarget.paramValue = data[i]
            nextTarget.params = Object.assign({}, params)
            nextTarget.textParams = Object.assign({}, params)
            // console.lo('nextTarget', nextTarget)
            delete nextTarget['v-for']
            targets.push(nextTarget)
          }
        } else if (typeof data === 'object') {
          // obj
          const keys = Object.keys(data)
          data = Object.values(data)
          for (let i = data.length - 1; i >= 0; i--) {
            // tar.params = {}
            let nextTarget = Object.assign({}, tar)
            const params = {}
            const textParams = {}
            const keys = Object.values(target.target)
            params[keys[0]] = data[i]
            console.log('chh', data[i], data)
            // textParams[key[0]] =
            if (keys.length === 2) {
              params[keys[1]] = keys[i]
            }
            nextTarget.paramIndex = keys[i]
            nextTarget.paramValue = data[i]
            nextTarget.params = Object.assign({}, params)
            // console.lo('nextTarget', nextTarget)
            delete nextTarget['v-for']
            targets.push(nextTarget)
          }
        }
        continue
      } else {
        // func
      }
    }
    // 8-- v-for
    // v-if
    if (tar['v-if']) {
      let data = !!domProperty(tar['v-if'].right, tar.params)
      if (!data) {
        continue
      }
    }
    // 8-- v-if
    parseOutput.push(tar)
    if (tar.name === 'reserveText') {
      // console.log('tarValue:none', tar)
      const output = []
      for (let reserve of Object.values(tar.reserves)) {
        const strValueStart = tar.value.substr(0, reserve.start)
        const strValueEnd = tar.value.substr(reserve.end + 1, tar.value.length)
        if (reserve.type === 'function') {
          const get = domProperty(reserve.textRawValue, tar.params)
          // とりあえずglobalのみ対応
          const args = []
          const toStr = String(get)
          output.push(toStr)
        } else if (reserve.type === 'variable') {
          const get = domProperty(reserve.textRawValue, tar.params)
          let toStr = String(get)
          // tar.value = strValueStart + toStr + strValueEnd
          output.push(toStr)
          // console.log('pppRRR', reserve, tar.value, global)
        } else if (reserve.type === 'direct') {
          output.push(reserve.text)
        }
      }
      tar.value = output.join('')
    }
    if (option && option.mode === 'answerDOM') {
      if (option.existString) {
        if (tar.answer && tar.name === 'reserveText') {
          // とりあえずexistStringなので....
          // console.log('tarValue', tar, targetIndex)
          if (typeof lastOutput[outputIndex] !== 'string') {
            lastOutput[outputIndex] = ''
          }
          lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value
        } else if (tar.answer) {
          // console.log('tarValue:without', tar)
        }
        // -- lastPropagate
        // 子供に伝播
        if (tar.name === 'br') {
          outputIndex++
        }
        if (tar.open) {
          let closeObject = {}
          closeObject.open = false
          closeObject.close = true
          closeObject.name = tar.name
          closeObject.unique = tar.unique
          closeObject.depth = tar.depth
          targets.push(closeObject)
        }
        let pushChildren = tar.children || []
        for (let i = pushChildren.length - 1; i >= 0; i--) {
          const value = pushChildren[i]
          let nextObject = {}
          nextObject = Object.assign({}, value)
          if (tar.hasOwnProperty('params')) {
            nextObject.params = Object.assign({}, tar.params)
          }
          if (tar.hasOwnProperty('textParams')) {
            nextObject.textParams = Object.assign({}, tar.textParams)
          }
          if (tar.hasOwnProperty('paramIndex')) {
            nextObject.paramIndex = tar.paramIndex
          }
          if (tar.hasOwnProperty('answer')) {
            nextObject.answer = tar.answer
          }
          if (tar.name === 'answer') {
            nextObject.answer = true
            nextObject.answerIndex = i
          }
          // console.log('cheek', nextObject)
          targets.push(nextObject)
          // -- lastPrpagate
        }
      }
    }
  }

MainProcess(ジャッジシステムをする場所)では、vueのv-forやv-ifが機能しないので、
こちら側で、処理する必要があるので、その機能を少し拝借します。
v-forに注目すると、単純にfor文で回している以外に、params,textParams,parseParamsと、valueとindexを
ASTの中に忍ばせます。

こうすることで、次にdomPreviewParseをみてみると、

domPreviewParse.js
function domPreviewParse (domTree, fileName) {
  const output = []
  const parentParam = {}
  saveDomTree = domTree
  output.push('<div id="previewDOM">')
  const runVueCode = runVueDom(domTree)
  // ループが起きると困るので、userアクション(v-on:clickとか@clickとか)の時に最描画するようにする
  for (let i = 0; i < runVueCode.length; i++) {
    const take = runVueCode[i]
    const parseDom = {}
    const yoyaku = {}
    if (take.name === 'router-link') {
      take.routerPush = true
      parseDom.routerPush = '@router'
    }
    if (take.hasOwnProperty('class')) {
      let targetDom = []
      targetDom.push('\'' + fileName + '\'')
      if (take.class.hasOwnProperty('variables')) {
        take.class.variables.forEach(key => {
          targetDom.push('\'' + key + '\'')
        })
      }
      parseDom['v-bind:style'] = 'classEvent(' + targetDom.join(',') + ')'
    }
    if (take.hasOwnProperty('others')) {
      // 現状v-forとclassを分けたらいけるか...?
      for (let i = 0; i < take.others.length; i++) {
        if (take.others[i].left === 'v-for' || take.others[i].left === 'class' || take.others[i].left === 'href') {
          continue
        }
        let key = take.others[i].left
        if (take.others[i].directive) {
          key = ':' + key
        }
        let targetInput = ''
        const targetDom = []
        if (take.others[i].type) {
          let otherRight = take.others[i].right
          otherRight = otherRight.replace(/\'/g, '\\\'')
          targetDom.push('\'' + otherRight + '\'')
          targetDom.push('\'' + fileName + '\'')
          if (key.indexOf('click') >= 0) {
            targetDom.push('true')
          } else {
            targetDom.push('false')
          }
          // targetDom.push(Object.keys(parentParam))
          if (take.parseParams) {
            Object.keys(take.parseParams).forEach(key => {
              let value = take.parseParams[key]
              const valueType = typeof value
              if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) {
                value = '\'' + value + '\''
              }
              if (typeof value === 'string' && value.indexOf('[') > 0) {
                value = 'parseEvent(' + '\'' + value + '\'' + ')'
              }
              if (valueType === 'object') {
                const output = []
                let stack = [...Object.keys(value)]
                // while (stack.length > 0) {
                //   const takeKey = stack.pop()
                //   output.push(takeKey + ': ')
                //   output.push(value[takeKey] + ', ')
                // }
                for (let i = 0; i < stack.length; i++) {
                  output.push(stack[i] + ': ')
                  let outVal = value[stack[i]]
                  const typeVal = typeof outVal
                  if (typeof outVal === 'string') {
                    outVal = '\'' + outVal + '\' '
                  }
                  if (i !== stack.length - 1) {
                    output.push(outVal + ', ')
                  } else {
                    output.push(outVal)
                  }
                }
                value = output.join('')
                value = '\{ ' + value + '\}'
              }
              targetDom.push('\{' + key + ': ' + value + '\}')
            })
          }
          targetInput = 'domEvent(' + targetDom.join(',') + ')'
        }
        parseDom[key] = targetInput
      }
    }
    if (take.name === 'reserveText' && take.reserves) {
      const textOutput = []
      for (let i = 0; i < take.reserves.length; i++) {
        const reserveVal = take.reserves[i]
        if (reserveVal.type === 'direct') {
          textOutput.push(reserveVal.textRawValue)
        } else {
          const targetDom = []
          targetDom.push('\'' + reserveVal.textRawValue + '\'')
          targetDom.push('\'' + fileName + '\'')
          targetDom.push('false')
          if (take.parseParams) {
            Object.keys(take.parseParams).forEach(key => {
              let value = take.parseParams[key]
              const valueType = typeof value
              if (valueType !== 'number' && valueType !== 'boolean' && valueType !== 'object' && value && (!value.indexOf('[') > 0 && !value.indexOf(']') > 0)) {
                value = '\'' + value + '\''
              }
              if (typeof value === 'string' && value.indexOf('[') > 0) {
                value = 'parseEvent(' + '\'' + value + '\'' + ')'
              }
              if (valueType === 'object') {
                const output = []
                let stack = [...Object.keys(value)]
                output.push('{ ')

                for (let i = 0; i < stack.length; i++) {
                  output.push(stack[i] + ': ')
                  let outVal = value[stack[i]]
                  const typeVal = typeof outVal
                  if (typeof outVal === 'string') {
                    outVal = '\'' + outVal + '\''
                  }
                  if (i !== stack.length - 1) {
                    output.push(outVal + ', ')
                  } else {
                    output.push(outVal)
                  }
                }
                output.push(' }')
                value = output.join('')
              }
              targetDom.push('\{' + key + ': ' + value + '\}')
            })
          }
          textOutput.push('\{\{ domEvent\(' + targetDom.join(', ') + '\) \}\}')
        }
      }
      parseDom.reserveText = textOutput.join('')
    }
    // --各々の作用
    if (!parseDom.hasOwnProperty('reserveText')) {
      const pushOutput = []
      for (let i = 0; i < Object.keys(parseDom).length; i++) {
        const key = Object.keys(parseDom)[i]
        if (key === 'routerPush') {
          let toParam = {}
          if (Object.keys(parseDom).indexOf(':to') >= 0) {
            toParam = parseDom[':to']
          }
          if (Object.keys(parseDom).indexOf('to') >= 0) {
            toParam = parseDom['to']
          }
          if (Object.keys(toParam).length == 0) {
            continue
          }
          pushOutput.push('@click' + '="' + 'routerEvent(' + toParam + ')' + '"')
          continue
        }
        pushOutput.push(key + '="' + parseDom[key] + '"')
      }
      let endBlock = '>'
      if (!take.open && !take.close) {
        endBlock = '/>'
      }
      let startBlock = '<'
      if (take.close) {
        startBlock = '</'
      }
      output.push(startBlock + take.name + ' ' + pushOutput.join(' ') + endBlock)
    } else {
      output.push(parseDom.reserveText)
    }
  }
  console.log('output', output)
  output.push('</div>')
  return output.join('')
}

先ほどと違い、parseParamsがあるので、それを引数として渡すことで、確実に値渡しとなり、正常に描画されます。
ただ、v-for,v-ifを扱ってないのでvueと同じ様に動的レンダリングするためには、こちら側でレンダリングをさせるようにしないといけません。
レンダリング自体は、globalの値が変更されたものをMainProcess -> domPreviewParseに潜らせれば、レンダリングするので、それをいつ発火させるかです。
今回の場合は、他からの要因である発火イベントで発火するようにしたい(でないと、制約をつけるかしないとレンダリングループしますからね)ので、(今回の場合は)clickイベントとなるような、@clickv-on:clickにたいして、

click
if (key.indexOf('click') >= 0) {
            targetDom.push('true')
          } else {
            targetDom.push('false')
          }

domEventの第三引数を予約する形にし、ここがtrueの場合処理が終わり次第描画するようにすることで、
元々表現したかった、userのvue.jsコードでの安全な描画ができるようになりました。

##プレビュー機能

デモ:
Homeに流し込んだモノ

PreviewField
上記でたいたいの大枠は説明したので、補足として説明いたします。

previewParse
previewParse: function () {
      const getDDD = this.outputDom
      const self = this
      const domEvent = this.domEvent
      const classEvent = this.classEvent
      const parseEvent = this.parseEvent
      const routerEvent = this.routerEvent
      const testSumple = getDDD
      bootstrapImports()
      let newPreviewDom = Vue.component('newPreviewDom', {
        template: getDDD,
        methods: {
          domEvent: domEvent,
          classEvent: classEvent,
          parseEvent: parseEvent,
          routerEvent: routerEvent
        },
        components: {
          Answer,
          PreviewCard,
          AnswerCard,
          'router-link': Item,
          ...importBootstrap
        }
      })
      let vm = new Vue({
        Answer,
        render: h => h(newPreviewDom)
      })
      // this.pushPreview = newPreviewDom
      const targetDomChange = document.getElementById(this.uniqueKey).children[0]
      vm.$mount(targetDomChange)
      this.$emit('vueDom', vm.$el, vm.$el.children)
    }

色々調べると、vue.jsの再$mountの様な記事はあるのですが、
あまりこれと言った記事はでませんでした。
(例えばvueではなく、vueに生の仮想DOMでない生のDOMを載せるモノや、少し古いモノや今回の意図した挙動ではないものが散見しているように思えます)(多分ですが、userのvue.jsをある程度安全に最描画させるという方針でないと、上記のv-for参照渡し問題や、jsのevalセキュリティヤバい問題と衝突するからなのではというのと、vue.jsはそれだけでリッチなので今回の様なプロダクトを目指さなければ他の機能で代用できることが原因かと思われます。)
なので、私と同じ様な問題に衝突した人がいれば、今回のpreview画面は参考になると思われますし、質問があれば是非答えたいと思います。

vue.jsが#appにmountしているように、新たに描画するものにid指定でmountしたいと考えます。
また、今回プロジェクト単位拡張がありますので、idを固定値で指定してしまうと、最初のモノしかレンダリングされないので、propsとして引き受ける様にします。

templateには、上記で行ったvue.jsを再現できるコードにparseしたものを載せ、
methodsには、domEvent,classEvent,parseEvent,routerEventを用意し、これらを踏み台として、globalへのアクセスを可能とさせます。
componentsには、ジャッジシステムのためのAnswer,PreviewCard,AnswerCardと、userがプロジェクト単位拡張問題で書くであろう、'router-view'をこちら側のコンポーネントと紐付けます。
そしてさらにbootstrapを扱えた方がなにかと良いと思ったので、全てをコンポーネントとして読み込むことで、(Vue.use機能は作成したjsインタプリターには機能として載せてないので)表現しています。

##プロジェクト単位拡張

デモ:
Homeに流し込んだソースコード
Exam5Detailに流し込んだソースコード
router設定に流し込んだソースコード

ProjectProcess
routerProcess

一週間の開発期間の後、jphacksでオンラインジャッジシステムを作っているtrack様からこの様なFBを頂きました。

個人的には割と好きなんですが、システムが使えるかどうかという観点で見ると、フロントエンドエンジニア向け製品というにはちょっと遠いですかね。システムとしては、Vue.js (のコンポーネント) のレンダリング結果を評価しているように見えます。逆に言えば、今のシステムはそれしかできなさそう。もちろん、そうした評価もフロントエンド開発における自動テストの一部を担ってはいるのですが、一部でしかなく。

具体的には画面操作を行った時の遷移だとかを評価できそうには見えないので、フロントエンドエンジニアの評価って話としてはまだ風呂敷広げすぎかな、と

ユーザの書いたコードをVueのコンパイラに食わせてUnitTestしているだけだと思う。基本的なアイデアはtrackとまるっきり一緒です。frontendの評価システムとして最近よくあるのは画面のスナップショット取って正解画像との比較をするっていうやり方ですね。progateとかもやってるんじゃなかったかな。

unit(単体)テストやら、画面遷移やら出来ないし、この先も出来ないんじゃないの? (意訳())
じゃ作ってやろうと思いプロジェクト単位の問題を作ることを決心しました。

vue-routerの設定が出来る。それらを使い遷移することが出来る
ができれば単体間の相互作用と、遷移が存在することができれば上記のFBは解決できそうだと感じたので、
これをプロジェクト単位拡張と定義します(実際にはこれら以外にもターミナル機能つけたり、ブラウザのようなタブを増やすことができるようにもしました)

プロジェクト単位に拡張したので、全てのプロジェクト単位の情報を入れる箱が必要なので、
earthというものを定義します globalはもう使っちゃってたので(ぇ

ProjectProcess.js
let earth = { pages: {}, targetURL: '/', baseURL: 'localhost:8080', router: {}, earchParam: {}, designChecker: {} } // プロジェクト単位

pagesには、

ProjectProcess.js
function pageAdd (pageName, template, script, style, domTree, pure, global) {
  earth.pages[pageName] = {}
  if (template) {
    earth.pages[pageName].template = template
  }
  if (script) {
    earth.pages[pageName].script = script
  }
  if (style) {
    earth.pages[pageName].style = style
  }
  if (pure) {
    earth.pages[pageName].pure = pure
  }
  if (domTree) {
    earth.pages[pageName].domTree = domTree
  }
  if (global) {
    earth.pages[pageName].global = global
  }
  earth.pages[pageName].pageName = pageName
  if (!earth.pages[pageName].hasOwnProperty('url')) {
    earth.pages[pageName].url = ''
  }
  console.log('done:PageAdd!!', earth)
}

新たに提出画面に、ページ追加タブを追加し、
template ~ globalまでの、情報を格納できるようにします。
また、のちにrouterからurlが紐付けられると思うので、それを受け入れられる様にurlを空で初期化しときます。

targetURLは、現在プロジェクト単位問題で表示しているエンドポイントを格納しときます。
baseURLは、今回はlocalhost:8080で初期化しときますが、一応変更できる様に、変数として持っときます。
次にrouterの設定ですが、
routerProcess.jsのrouterProcessに流し込みます。

routerProcess
function routerProcess (text) {
  const script = text
  const routerAst = routerCreateAST(script)
  const astList = routerAst.program.body
  const router = {}
  console.log('astList', astList)
  const pages = Object.assign({}, earth.pages)
  const toPages = {} // とりあえず文字列として渡す?
  Object.keys(pages).forEach(key => {
    toPages[key] = key
  })
  toPages.Home = 'Home'
  toPages.ProblemList = 'ProblemList'
  toPages.baseURL = earth.baseURL
  for (let i = 0; i < astList.length; i++) {
    const value = astList[i]
    console.log('value', value)
    if (value.type === 'VariableDeclaration') {
      for (let i = 0; i < value.declarations.length; i++) {
        let decVal = value.declarations[i]
        let valInit = decVal.init
        if (valInit.type === 'NewExpression') {
          // maybeargument one?
          valInit = valInit.arguments[0]
          let childRouter = {}
          console.log('valInit', valInit, value)
          for (let i = 0; i < valInit.properties.length; i++) {
            if (valInit.properties[i].key.name === valInit.properties[i].value.name && valInit.properties[i].value.type === 'Identifier') {
              // routes?
              const getRouteKey = valInit.properties[i].key.name
              childRouter = Object.assign(childRouter, getProperty(valInit.properties[i].value, toPages))
              childRouter[getRouteKey] = routerPushFunc(childRouter[getRouteKey])
              continue
            }
            childRouter[valInit.properties[i].key.name] = getProperty(valInit.properties[i].value, toPages)
          }
          console.log('router', childRouter, decVal.id.name)
          router[decVal.id.name] = childRouter
          continue
        }
        const valRoute = getProperty(valInit, toPages)
        console.log('decVal.id.name', decVal.id.name, valRoute, decVal)
        router[decVal.id.name] = valRoute
        const toRouter = Object.assign({}, router)
        toPages[decVal.id.name] = toRouter
      }
    } else if (value.type === 'ExportDefaultDeclaration') {
      earth[value.declaration.name] = router[value.declaration.name]
      if (value.declaration.name === 'router') {
        // routerの時に特殊な処理
        const routerVal = router.router
        if (routerVal.routes && routerVal.routes.component) {
          for (let i = 0; i < Object.keys(routerVal.routes.component).length; i++) {
            const key = Object.keys(routerVal.routes.component)[i]
            if (earth.pages[key]) {
              const endpoint = routerVal.routes.component[key].path
              earth.pages[key].endpoint = endpoint
              earth.pages[key].url = routerVal.base + endpoint
              render()
            }
          }
        }
        outputRouterString = outputRouterInfo()
      }
    }
  }
}

function routerPushFunc (arg) {
  const path = {}
  const name = {}
  const component = {}
  let pure = []
  pure = [...arg]
  for (let i = 0; i < arg.length; i++) {
    const val = arg[i]
    if (arg[i].hasOwnProperty('name')) {
      name[val.name] = val
    }
    if (arg[i].hasOwnProperty('path')) {
      path[val.path] = val
    }
    if (arg[i].hasOwnProperty('component')) {
      component[val.component] = val
    }
  }
  return { path: path, name: name, component: component, pure: pure }
}

今まで同様textベースで貰ったものを、型に落とし込み、
その後、earthのrouterに流し込みます。
また、

export default router

のようにexportされる作りになるはずなので、
これが実行されたら、各々の登録されているページにrouterで設定したrouter情報を紐付けます。

実際のページ遷移がどのように行われているかというと、
オンラインジャッジする時には、レンダリングはしていないので、こちら側でターゲットとしているページを変更すれば良いだけなので解説しませんが、実際にレンダリングを行っているプレビューがどのように行われているかは下記のようになります。

ProblemDetail.vue
    routerChange: function (param) {
      // name か pathか調べる
      const keys = Object.keys(param)
      let params = {}
      if (keys.indexOf('params')) {
        params.$route = {}
        params.$route.params = param.params
      }
      if (keys.indexOf('name') >= 0 && keys.indexOf('path') >= 0) {
        console.error('both name and path exist.')
        return false
      } else if (keys.indexOf('name') >= 0) {
        if (earth && earth.router && earth.router.routes && earth.router.routes.name[param.name]) {
          const routeInfo = earth.router.routes.name[param.name]
          const componentName = routeInfo.component
          if (earth.pages[componentName]) {
            this.sumpleTest(earth.pages[componentName].pure, params)
          }
        }
      } else if (keys.indexOf('path') >= 0) {
      }
    }

router-linkなどは上記三つを組み合わせて、vue.js単一ファイルとして機能させるで、routerEventが設定されるようになってますので、それで上のコードが発火される様になっています。
上のコードは、まずそれがどのような飛び方をするか調べています。nameで飛ぶのかpathで飛ぶのか調べたのち(ハッカソンなのでnameしか実装してないですが、pathも同様に行えば可能)

routerInfoでrouterに格納している情報を取り出し、次に表示するページを準備します。
またvueでのrouterでの遷移は特定のitemを受け渡すことが可能ですので、

if (keys.indexOf('params')) {
        params.$route = {}
        params.$route.params = param.params
      }

としてparamsを渡すことで、相手page側のglobalへ、あげたい情報を流し込むことができます。(これでthis.$route 系の情報を引っ張ることができるようになります。)
(MainProcess汎用的に作っててよかったです...)

これでデモの様に、
ホームに流し込んだソースコードだけで機能するが、
routerとページ追加をして初めて遷移し、$route.paramsの情報を取得できるようになる

を表現することができました。

##ジャッジシステム/スタイルジャッジシステム

デモ:
Homeに流し込んだモノ

ジャッジシステム
スタイルジャッジシステム:emitDom

まず、ジャッジシステムから、みていきます。
最初の一週間で仕上げにいかなければいけなかったので(ファイナリスト選抜があるので)、
文字列の探索のみ実装しています。(それ以降はプレビュー機能やプロジェクト拡張などがあるので、一ヶ月ではこちらに力を割ける時間がなかったです。)

MainProcess.js
if (option && option.mode === 'answerDOM') {
      if (option.existString) {
        if (tar.answer && tar.name === 'reserveText') {
          // とりあえずexistStringなので....
          // console.log('tarValue', tar, targetIndex)
          if (typeof lastOutput[outputIndex] !== 'string') {
            lastOutput[outputIndex] = ''
          }
          lastOutput[outputIndex] = lastOutput[outputIndex] + tar.value
        } else if (tar.answer) {
          // console.log('tarValue:without', tar)
        }
        // -- lastPropagate
        // 子供に伝播
        if (tar.name === 'br') {
          outputIndex++
        }
        if (tar.open) {
          let closeObject = {}
          closeObject.open = false
          closeObject.close = true
          closeObject.name = tar.name
          closeObject.unique = tar.unique
          closeObject.depth = tar.depth
          targets.push(closeObject)
        }
        let pushChildren = tar.children || []
        for (let i = pushChildren.length - 1; i >= 0; i--) {
          const value = pushChildren[i]
          let nextObject = {}
          nextObject = Object.assign({}, value)
          if (tar.hasOwnProperty('params')) {
            nextObject.params = Object.assign({}, tar.params)
          }
          if (tar.hasOwnProperty('textParams')) {
            nextObject.textParams = Object.assign({}, tar.textParams)
          }
          if (tar.hasOwnProperty('paramIndex')) {
            nextObject.paramIndex = tar.paramIndex
          }
          if (tar.hasOwnProperty('answer')) {
            nextObject.answer = tar.answer
          }
          if (tar.name === 'answer') {
            nextObject.answer = true
            nextObject.answerIndex = i
          }
          // console.log('cheek', nextObject)
          targets.push(nextObject)
          // -- lastPrpagate
        }
      }
    }
  }

domとして、answerで挟まれているものを見つけ、
それらに対して文字列を見つけます。また正解のものが配列の場合、
があることで、
次のindexのものに進んで見つけます。
このようにして、複数のテストケースに対して正答しているかを調べます。

次にデザインチェックに対してみていきます。

emitDom
emitDom: function () {
      // console.log('previewDom', value, value.children, value.children[0])
      const value = this.checkStyleDom
      console.log('previewDom:func', value.children[0], value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].getBoundingClientRect(), [value.children[0]])
      console.log('preview:style', value.children[0].children[0].children[0].getBoundingClientRect(), value.children[0].children[1].children[0].getBoundingClientRect(), value.children[0].children[2].children[0].getBoundingClientRect())
      let targetStyle = this.getExam.examInfo
      let targetBool = true
      if (targetStyle && targetStyle.option && targetStyle.option.styleCheck) {
        targetStyle = targetStyle.option.styleCheck
      } else {
        this.checked = true
        this.clickFlug = true
        return true
      }
      if (!targetStyle.hasOwnProperty('children')) {
        // bugでroot層だけchildrenがないパターン(必要なのに)ないパターンがある
        targetStyle.children = {}
        Object.keys(targetStyle).forEach(key => {
          if (key !== 'count' && key !== 'style' && key !== 'children') {
            targetStyle.children[key] = targetStyle[key]
          }
        })
      }
      let que = [targetStyle]
      let domQue = [value.children[0]]
      while (que.length > 0) {
        // 正答判定
        let take = que.shift()
        let countDomTake = []
        if (take.count > 0) {
          for (let i = 0; i < take.count; i++) {
            countDomTake.push(domQue.shift())
          }
        }
        console.log('ccck', take, countDomTake)
        const diffStyleCheck = {}
        const diffStyles = []
        let NextChild = countDomTake[0]
        for (let i = 0; i < countDomTake.length; i++) {
          let domTake = countDomTake[i]
          let domStyle = domTake.getBoundingClientRect()
          let domRawStyle = countDomTake[i].style
          if (!take.hasOwnProperty('name')) {
            // noname
          } else {
            // nameつき
            if (take.name === 'AnswerCard') {
              domTake = countDomTake[i].children[0]
              NextChild = countDomTake[0].children[0]
            }
          }
          console.log('countDomTake', domTake, domStyle)
          diffStyles.push(domStyle)
          if (take.hasOwnProperty('style')) {
            for (let parentKey of Object.keys(take.style)) {
              // _区切りでor判定とする
              console.log('take.style', parentKey, take.style)
              const splitKeys = parentKey.split('_')
              let splitBool = []
              for (let i = 0; i < splitKeys.length; i++) {
                const key = splitKeys[i]
                for (let subKey of Object.keys(take.style[key])) {
                  console.log('subKey', subKey, domStyle, key)
                  if (subKey === 'max' || subKey === 'min') {
                    // 幅指定
                    if (subKey.match('max')) {
                      // minの時だけ判定
                      continue
                    }
                    if (domStyle[key]) {
                      // 他に依存しない
                      if (take.style[key].min <= domStyle[key] && domStyle[key] <= take.style[key].max) {
                        continue
                      } else {
                        splitBool.push(false)
                      }
                    } else {
                      // 他要素と依存関係にあるstylecheck
                      diffStyleCheck[parentKey] = true
                    }
                  } else if (!(subKey === domRawStyle[key])) {
                    // absolute指定
                    if (key === 'overflow') {
                      // 例外処理
                      console.log('overflow', key)
                      const upperSubKey = subKey.toUpperCase()
                      if (domRawStyle[key + upperSubKey]) {
                        console.log('overflow', domRawStyle[key + upperSubKey])
                      } else {
                        console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]])
                        splitBool.push(false)
                        this.checkData.reason = "absolute指定:アウト"
                        this.clickFlug = true
                        this.reason = "absolute指定:アウト"
                      }
                    } else {
                      console.log('absolute指定:アウト', subKey, domRawStyle[key], [domRawStyle], [countDomTake[i]])
                      splitBool.push(false)
                      this.checkData.reason = "absolute指定:アウト"
                      this.clickFlug = true
                      this.reason = "absolute指定:アウト"
                    }
                  } else {
                    // trueをいれとく
                    splitBool.push(true)
                  }
                }
                let continueBool = false
                for (let take of splitBool) {
                  if (take) {
                    continueBool = true
                    break
                  }
                }
                if (continueBool || splitBool.length == 0) {
                  continue
                }
                // false
                this.checked = false
                console.log('style:False', splitBool, take, [domTake])
                this.clickFlug = true
                return false
              }
            }
          }
        }
        if (diffStyles.length > 0) {
          let xDiffs = [...diffStyles]
          let yDiffs = [...diffStyles]
          for (let i = 0; i < xDiffs.length; i++) {
            xDiffs[i].index = i
            yDiffs[i].index = i
          }
          xDiffs.sort((a, b) => a.x - b.x)
          yDiffs.sort((a, b) => a.y - b.y)
          for (let i = 1; i < xDiffs.length; i++) {
            const xDiff = xDiffs[i].x - (xDiffs[i - 1].x + xDiffs[i - 1].width)
            const yDiff = yDiffs[i].y - (yDiffs[i - 1].y + yDiffs[i - 1].height)
            xDiffs[i - 1].xDiffRight = xDiff // 右側との差
            xDiffs[i].xDiffLeft = xDiff // 左側との差
            yDiffs[i - 1].yDiffBottom = yDiff // 下側との差
            yDiffs[i].yDiffTop = yDiff // 上側との差
          }
          let orders = Object.keys(diffStyleCheck)
          console.log('orders', orders, diffStyles, countDomTake)
          for (let order of orders) {
            let splitOrders = order.split('_')
            const splitBool = []
            console.log('order', order, xDiffs)
            for (let key of splitOrders) {
              const max = take.style[order].max
              const min = take.style[order].min
              console.log('checcker', min, max, key)
              switch (key) {
                case 'padding':
                case 'margin':
                  // とりあえずこれらをまとめてお互いの距離感として処理する
                  // とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で
                  let marginCheck = true
                  console.log('paddingOrMargin', xDiffs, key)
                  for (let i = 0; i < xDiffs.length; i++) {
                    console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft)
                    if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') {
                      console.log('xDiffLeft', xDiffs[i])
                      if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) {
                        console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft)
                        marginCheck = false
                        break
                      }
                    }
                    if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') {
                      console.log('xDiffRight', xDiffs[i])
                      if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) {
                        console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight)
                        marginCheck = false
                        break
                      }
                    }
                  }
                  splitBool.push(marginCheck)
                  break
              }
            }
            let checkSplitBool = false
            splitBool.forEach(flag => {
              if (flag) {
                checkSplitBool = true
              }
            })
            if (!checkSplitBool && splitBool.length > 0) {
              this.checked = false
              this.clickFlug = true
              return false
            }
          }
        }
        if (NextChild && NextChild.children) {
          domQue.push(...NextChild.children)
        } else {
        }
        if (take.hasOwnProperty('children')) {
          console.log('take.children', take.children)
          que.push(...Object.values(take.children))
        }
      }
      for (let child of value.children[0].children) {
        console.log('previewDom:dom', child.children[0], child.children[0].getBoundingClientRect())
      }
      this.previewDom = value
      this.checkFlug = true
      this.clickFlug = true
    },

デザインチェックボタンでイベント発火し、emitDomが呼ばれます。
ここでは、その問題に紐付けられたスタイルチェック用の情報と照らし合わせにいきますが、
ここでみている点は3つです。

・一つはそれが固有に持たなければ不正解になる要素
(overlayやdisplayなど)
にたいして、そのdomをみて、持っているかどうかを判定します(設定されたものをみるのではなく、レンダリングされたものをみます)

・次に他に依存しないサイズ指定があるもの
(widthやheight)に対して、それがある指定されているサイズに対して、クリアしているかどうかをみます
(例えばwidthやheightの場合、データベースには minとmaxが設定されています。その間に要素のサイズが入ってるかどうかをみます)

・次に他に依存するもの
(paddingやmargin)などに対して、それらの間の距離をみて判断します。
v-forなどで連鎖している要素に対しての、差の情報がminとmaxで入っているので
レンダリングされた情報から取得します。

padding-margin
case 'padding':
                case 'margin':
                  // とりあえずこれらをまとめてお互いの距離感として処理する
                  // とりあえず左右だけ見るようにする -> 縦軸も一応取得してるから、見たい時は違う命令で
                  let marginCheck = true
                  console.log('paddingOrMargin', xDiffs, key)
                  for (let i = 0; i < xDiffs.length; i++) {
                    console.log('xDiffs', xDiffs[i], xDiffs[i].xDiffLeft)
                    if (xDiffs[i].xDiffLeft || typeof xDiffs[i].xDiffLeft === 'number') {
                      console.log('xDiffLeft', xDiffs[i])
                      if (!(min <= xDiffs[i].xDiffLeft && xDiffs[i].xDiffLeft <= max)) {
                        console.log('style:DiffFalseLeft', key, xDiffs[i], min, max, xDiffs[i].xDiffLeft)
                        marginCheck = false
                        break
                      }
                    }
                    if (xDiffs[i].xDiffRight || typeof xDiffs[i].xDiffRight === 'number') {
                      console.log('xDiffRight', xDiffs[i])
                      if (!(min <= xDiffs[i].xDiffRight && xDiffs[i].xDiffRight <= max)) {
                        console.log('style:DiffFalseRight', key, xDiffs[i], min, max, xDiffs[i].xDiffRight)
                        marginCheck = false
                        break
                      }
                    }
                  }
                  splitBool.push(marginCheck)
                  break
              }

jphacksの審査員や、チームメンバーすらも
これらの情報の取得がレンダリング後の結果ではなく、
単純にcssをみていると勘違いしていたので、ここだけは強く主張したいです()

(まあ普通に考えたら、cssの方が楽だからそっちで実装したとおもわれるよね..)

#後書き

作成したサービスに対してはある程度説明できたので、ここからは
ハッカソンとしての反省点などを書こうと思います。
また、僕が作ったもの以外もターミナル機能や、マイページ機能、レーティング機能など素晴らしいものがあるので、チームメンバーがそれらに対する記事などを書いてもらったら載せようかと思います。

また再度になりますが、リアクションもらえたり質問をもらえたりすると嬉しいのでLGTMや質問ガンガンもらえたら嬉しいです。よろしくお願いします。

##反省点
まず僕が担当した部分担当していない部分を総括しても、よく完成したプロダクトだと思いますし、
技術力という点に対してもhackdayを取ったサービスや、そのほかの賞を受賞したプロダクトとひけをとらないと思います。
ただ確かな反省点は二点ほどあります。

###ほかのプロダクトと比較して華がない

製品としてほかのものは最新技術を看板にしたものや、
プレゼン込みでプレゼンが華やかになるように製品を作って行った様に思えます。
僕たちのプロダクトはプレゼンで華やかにするのは現状難しいものだと思うので、もう少し上記の様なソースコードの解説や中身をみなくても、わかるようなものをAwardDayで仕上げるべきだったと思います。

###jphacksスポンサー企業との接点がない

僕たちの製品はファイナリストに出場したのですが、その前のHackDay(開発期間一週間でプレゼンする)では、
賞などにかすりもせず、ファイナリストに出場する形となりました。
ファイナリストの出場条件はコード審査(jphacks側での)なので通ったかと思います。
ただ、AwardDayでは、コード審査はなく、プレゼン審査のみとなります。そうなるとスポンサー企業の企業賞というのは効力をもつようになります。
コード審査の場合はjphacks側のみしかみてないので、ファイナリストを他との兼ね合いを考えずにできると思いますがプレゼン審査なので、ある程度他が審議したものの評価もいれないといけません。
そうなると、複数の特定企業に刺さるプロダクトを目指す方向性に、HackDayが終了し次第、うつるべきだったと今では感じます。
僕たちは、貰ったFBを反映する形にしたのですがよくよく考えたら、track開発チーム様は審査員ではないので、そちらのFBを反映するよりも、もう少しペルソナやターゲット層(今回はスポンサー様)に向けるべきだったなと感じます。
ハッカソンというのは、ビジネスソン(isuconとかと違いプロダクトを作りそれを評価するというファジィなものなので)よりな背景もあって然るべきとも思うので、今回は上記のプロダクトとしての華や、ターゲット層の誤りなどビジネス的な観点が今回の大きな敗因だと思います。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?