goldblendでCoffeeScriptのクラスを生成する話の最終話

#環境
* node.js 0.10.33
* express 4.0.0-rc4
* CoffeeScript 1.9.2

#経緯
[昼に書いた奴](http://qiita.com/mrpepper/items/5750257057e44cb01fb3)の続きです。なぬっ。今北産業とな？　おぬし、お里が知れるぞ。

```lang:imakita3gyo
　だばだー
　だーば
　だばだー
```

いや、失敬。

```lang:imakita3gyo
　画面遷移を
　変な構文で短く書いて
　CoffeeScript生成
```

もうだいぶ壊れているので、詳しい話は前記事と前々記事を見てくださいね。

#で、結局どうなったか
震えながら、-w オプションを試してみたら、何事もなく機能したので、coffee -> jsまでやらせることにしました。

なので結局、使い方は、後述のgoldblend.coffeeを適当な場所に置き、

```coffee ./goldblend.coffee -e -w mytest.gold &
```
以上です。mytest.goldが更新される度に、mytest.coffeeが出力され、mytest.jsが生成されます。常駐させないで都度やる場合は、今まで同様、-wを付けなければOKです。

あとは、設定行に「viewengine:」という設定項目ができました。これと、views/__template.jadeとか（拡張子は諸々読み替えてね）を作っておくと、views/classname.jadeなんかを自動的に作ってくれます。1行目に「updateとかなんとか」というコメントが挿入されるので、その行を削除してから編集して下さい。

多くの画面はとりあえずほっといて後で作る場合が多い＆最初に作りこんだ画面で「ああ、こういうふうにしとかなきゃいかんな」と思いついたことを「__template.jade」に反映させると思いますので、自動コピーしたまま手を触れていない（＝1行目を消していない）ファイルは、プリプロセス時に上書きされます。

#では、だばだを聴こう！
「[例の曲](https://www.youtube.com/watch?v=gDVd6n-WTW8)」の時間ですよ！　だばだー、だーーば、だばだー。敢えて説明はしませんが、設定行を処理したときに挿入されるコメントのデリミタがカンマになったり、ちょっと苦しいやり方で、画面定義クラスの下のスコープ中のコメントアウトと、画面定義の次のセクションの始めのコメントを見分けるようにしました。ほんと苦しい。というわけで、画面定義にぶら下がるスコープ中に、インデントされていない1行コメントは入れないようにして下さい。

この問題があるので、CoffeeScript -> JavaScriptのコンパイルでは、行コメントを消してしまうんだな、というのがよく分かりました。なるほどな。細かい使い方は再掲しません。プリプロセッサ「goldblend.coffee」の最新版をご覧下さい。

なんだかんだこいつを育てている間に、app.goldの雛形と、viewsの一連の雛形と、public/jsのjquery等のローカルコピーとかができました。（なお、余談ですが、views/layout.jadeとかあると思いますが、アレはviews/__layout.jadeとかに名前を変えた方が良いですよ。error.jadeも同じです。名前の衝突って、存外心が荒むものです）で、ここからもう一手間かけるなら、express-goldblend-generator.coffeeでも作れば充分過ぎだと思いますが、それにしても、 **どっぱどっぱ焦茶色の粉が出てくる感じ** のファイル名ですな。

```coffeescript:goldblend.coffee
fs = require('fs')

#------------------------------------
#-- コマンドライン引数の解釈
for val,index in process.argv
  if index>1
    if /^-/.test(val)
      if /w/.test(val)
        watch = true
    else
      if /\.gold$/i.test(val) and fs.existsSync val
        filesrc = val
        filedst = val.replace /gold$/i, 'coffee'

if not(filesrc?)
  console.log 'mi a ta ra naaai!'
  process.exit 1


#------------------------------------
#-- 成果物のjsへのコンパイル
coffee = require('coffee-script').compile

compile = (filedst) ->
  read = fs.readFileSync filedst
  jssrc = coffee read.toString(),{bare:true}
  fs.writeFileSync filedst.replace(/coffee$/,'js'),jssrc,'utf8'


#------------------------------------
#-- テンプレートファイルのコピー
filecopy = (classname,cvparams,viewengine) ->
  docopy = false
  filename = 'views/'+classname+'.'+viewengine
  template = 'views/__template.'+viewengine

  cv = cvparams.replace /^\s*,/, ''
  switch viewengine
    when 'ect'
      regex = /^<!--update/
      text = "<!--update:この行を消さないと、毎回上書きされます-->\n"
      text += "<!--このテンプレートの自明な引数 ⇒ #{cv} -->\n"
    when 'jade'
      regex = /^\/\/update/
      text = "//update:この行を消さないと、毎回上書きされます\n"
      text += "//このテンプレートの自明な引数 ⇒ #{cv}\n"
    when 'ejs'
      regex = /^<% \/\* update/
      text = "<% /* update:この行を消さないと、毎回上書きされます */ %>\n"
      text += "<% /* このテンプレートの自明な引数 ⇒ #{cv} */ %>\n"
    when 'coffee'
      regex = /^#update/
      text = "#update:この行を消さないと、毎回上書きされます\n"
      text += "#このテンプレートの自明な引数 ⇒ #{cv}\n"
    when 'haml'
      regex = /^-#update/
      text = "-#update:この行を消さないと、毎回上書きされます\n"
      text += "-#このテンプレートの自明な引数 ⇒ #{cv}\n"
    else
      console.log 'view engine ['+viewengine+'] is mi ta i o u!'
      reutrn
  
  if not fs.existsSync template
    console.log 'template file is not found!'
    return
  if fs.existsSync filename
    if regex.test fs.readFileSync(filename).toString()
      #コピー先が上書き許可なので、コピーする
      docopy = true
      fs.unlinkSync filename
    else
      #コピー先が有効なので、コピーしない
      docopy = false
  else
    #コピー先が存在しないので、コピーする
    docopy = true
    
  if docopy
    fs.writeFileSync(filename, text+fs.readFileSync(template, 'utf8'))
    console.log 'created: '+filename


#------------------------------------
#-- ファイル書き出し
writetext = (dst, text) ->
  fs.appendFileSync dst,text,'utf8', (err) -> console.log err


#------------------------------------
#-- 画面定義の出力（末尾）
generateTail = (dst, indentsp, classname, routername) ->
  text = indentsp+classname+'.install '+routername+"\n\n"
  writetext dst,text

#------------------------------------
#-- 画面定義の出力（前半）
generateMain = (dst,indentsp,classname,temp,routername,mw,nocache,viewengine) ->
  args = []
  for str,index in temp
    str = str.replace(/^\s+/, '').replace(/\s+$/, '')
    switch index
      when 0
        argreq = str
      when 1
        argres = str
      else
        args.push str
  urlprm = ''
  cvparams = ''
  buildparams4sp = ''
  reqparams4sp = ''
  yamlparams6sp = ''

  for str,index in args
    urlprm += '/:'+str
    cvparams += ','+str
    if index>0
      buildparams4sp += "\n"
      reqparams4sp += "\n"
      yamlparams6sp += "\n"
    buildparams4sp += "#{indentsp}    params += '/'+encodeURIComponent(#{str})"
    reqparams4sp += "#{indentsp}    #{str} = #{argreq}.params.#{str} ? ''"
    yamlparams6sp += "#{indentsp}      #{str}: #{str}"
  emptyobject = if args.length == 0 then ' {}' else ''

  randomstring = 'Math.random().toString(36).slice(-8)'
  appendnocache = if nocache then "+'?'+#{randomstring}+#{randomstring}" else ''
  path = classname.replace /__/,'/'
  
  text = """
#{indentsp}class #{classname}
#{indentsp}  @install: (obj) => obj.get '/#{path}#{urlprm}',#{mw}@get
#{indentsp}  @redirect: (#{argreq},#{argres}#{cvparams}) =>
#{indentsp}    params = ''
#{buildparams4sp}
#{indentsp}    #{argres}.redirect '/#{path}'+params#{appendnocache}
#{indentsp}  @get: (#{argreq},#{argres}) =>
#{reqparams4sp}
#{indentsp}    @direct #{argreq},#{argres}#{cvparams}
#{indentsp}  @direct: (#{argreq},#{argres}#{cvparams}) =>
#{indentsp}    params =#{emptyobject}
#{yamlparams6sp}
"""
  writetext dst,text
  filecopy classname,cvparams,viewengine
  return argres


#------------------------------------
#-- トークンの発見と処理
doconv = (dst, src) ->
  console.log 'processing... '+src
  content = fs.readFileSync(src).toString()
  content = content.replace(/\r\n/, "\n").replace(/\r/, "\n").split("\n")
  if fs.existsSync(dst)
    fs.writeFile dst, '', 'utf8', (err) -> console.log err
  comments = false
  generate = false
  indentsp = ''
  defaultmode = true
  routername = 'router'
  mw = ''
  nocache = true
  viewengine = 'ect'
  for line,index in content
    if index>0
      writetext dst,"\n"
    if comments
      if generate
        writetext dst,'  '+line
      else
        writetext dst,line
      if /^\s*###/.test(line)
        comments = false
    else
      normaloutput = false
      addindent = false
      
      result = line.match /^(\s*)#(\s*)/
      if result == null
        comsp = 0
        linecom = false
      else
        comsp = result[1].length+result[2].length
        linecom = true
      
      if /^\s*###/.test(line) #コメントのみ行を影響させない
        comments = true
        normaloutput = true
        addindent = generate
      else if linecom and ((not generate) or (comsp > indentsp.length))
        #コメント行は、コメントアウトと見なされる限りは無視
        normaloutput = true
        addindent = generate
      else if /^\s*$/.test(line) #空行を影響させない
        normaloutput = true
        addindent = generate
      else
        if /^\s*<<</.test(line)
          if generate
            #generate末尾出力
            generateTail dst,indentsp,classname,routername

          result = line.match /^(\s*)<<<\s*{([^{}]*)}/
          if result == null
            #newgenerate
            if defaultmode
              defaultmode = false
              confcomment = '#'+routername+', '
              confcomment += mw.replace(/,$/,'')+', '
              confcomment += nocache.toString()+', '
              confcomment += viewengine
              writetext dst,confcomment
            
            #indentspの取得
            #argreq,argres,argsの取得
            #classnameの取得
            result = line.match /^(\s*)<<<\s*(\S+)\s*\(([^()]+)\)/
            if result == null
              console.log 'syntax error ['+index+']'
              process.exit 1
            #generate要素読み取り
            indentsp = result[1]
            classname = result[2]
            temp = result[3].split(',')
            argres = generateMain dst,indentsp,classname,temp,routername,mw,nocache,viewengine
            generate = true
            normaloutput = false
          else
            #routernameの取得
            #mwの取得
            #nocache
            defaultmode = false
            scopeindentsp = result[1]
            tempkv = {}
            keyvalues = result[2].split(',')
            for keyvalue in keyvalues
              keyvalue = keyvalue.replace(/^\s+/, '').replace(/\s+$/, '')
              keyvalue = keyvalue.split(':')
              key = keyvalue[0].replace(/^\s+/, '').replace(/\s+$/, '')
              value = keyvalue[1].replace(/^\s+/, '').replace(/\s+$/, '')
              tempkv[key] = value
            if tempkv.routername?
              routername = tempkv.routername
              if routername.length == 0
                routername = 'router'
            if tempkv.middleware?
              mw = tempkv.middleware
              if mw.length > 0
                mw = mw + ','
            if tempkv.viewengine
              viewengine = tempkv.viewengine
              if viewengine = ''
                viewengine = 'ect'
            if tempkv.nocache?
              nocache = (/(true|ok|yes)/i.test(tempkv.nocache))
            confcomment = '#'+routername+', '
            confcomment += mw.replace(/,$/,'')+', '
            confcomment += nocache.toString()+', '
            confcomment += viewengine
            writetext dst,confcomment
            normaloutput = false
        else if generate
          spaces = line.match /^\s*/
          if spaces == null
            console.log 'a ri e nai error!'
            process.exit()
          if spaces[0].length > indentsp.length
            #generateスコープ内出力
            line = line.replace /@render\b/, argres+'.render'
            line = line.replace /@@/, "'"+classname+"'"
            writetext dst,'  '+line
            normaloutput = false
          else
            #generate末尾出力
            generateTail dst,indentsp,classname,routername
            generate = false
            normaloutput = true
        else
          normaloutput = true
      
      if normaloutput
        if addindent
          fs.appendFileSync dst,'  '+line,'utf8', (err) -> console.log err
        else
          fs.appendFileSync dst,line,'utf8', (err) -> console.log err
  console.log 'completed!'


#------------------------------------
#-- メインルーチン
doconv filedst,filesrc
compile filedst

if watch?
  fs.watchFile filesrc, (curr, prev) ->
    doconv filedst,filesrc
    compile filedst
else
  process.exit 0
```