sitateru tech blog: マークアップ

sitateru tech blog

シタテルの技術やエンジニアの取り組みを紹介するテックブログです。

ラベル マークアップ の投稿を表示しています。 すべての投稿を表示
ラベル マークアップ の投稿を表示しています。 すべての投稿を表示

2018年11月20日火曜日

独自マークアップ言語によるケアラベル(品質表示ネーム)エディター

11月 20, 2018

こんにちは、シタテルの茨木です。

衣服には必ずケアラベル(品質表示ネーム)というものがついています。洗濯の方法などが書いてあるアレですね。

凝ったデザインにすることもありますが、大体はパターンが決まっています。
今回は、独自のマークアップ言語でケアラベルを作れるようにしてみた、という内容です。

完成イメージ

  • 中央の入力欄で独自マークアップを入力すると、左にケアラベルがリアルタイムプレビューされる
  • 画像としてケアラベルをダウンロードできる
  • 右欄でCSSによるデザイン微調整が可能(おまけ)

ソースコード

https://github.com/tibaraki/care-label

目次

  1. マークアップをパースして配列に
  2. 配列からDOMを描画
  3. CSSを適用

マークアップをパースして配列に

本件の肝です。

パーサコンビネータparsimmonを使用します。

パーサコンビネータは文法要素の組み合わせで言語(文書)構造を定義していきます。
たとえば正規表現も文書構造を定義するものですが、正規表現は再帰が表現できないという限界があります。
今回使うようなパーサは再帰を記述できるので、理論上あらゆるプログラミング言語の文法チェックが可能です。

今回はこれをつかって、独自マークアップ言語の文法を定義してみます。

下記は入力例&出力例&パーサの本体です。
parseにマークアップを投げるとパース後の配列を返してくれます。

@id1
  aaa
@id2
  ccc
  @id2-1
    ddd
    eee
    @id2-1-1
      xxx
    fff
  @id2-2
    ggg
    hhh
[
  [
    "@id1",
    [
      [
        "aaa"
      ]
    ]
  ],
  [
    "@id2",
    [
      [
        "ccc"
      ],
      [
        "@id2-1",
        [
          [
            "ddd"
          ],
          [
            "eee"
          ],
          [
            "@id2-1-1",
            [
              [
                "xxx"
              ]
            ]
          ],
          [
            "fff"
          ]
        ]
      ],
      [
        "@id2-2",
        [
          [
            "ggg"
          ],
          [
            "hhh"
          ]
        ]
      ]
    ]
  ]
]
import {regex, string, lazy, seq} from 'parsimmon'

function lexeme(p) { return p.skip(regex(/[ \n]*/)) }

const lparen = lexeme(string('{'))
const rparen = lexeme(string('}'))

const elem = lazy('', () => { return block.or(line) })

const id   = lexeme(regex(/@[\w-]*/i))
const atom = regex(/[^\{\}\n ]+/).skip(regex(/ */))

const line  = regex(/[\n ]*/).then(atom.many()).skip(regex(/\n+/))
const block = regex(/[\n ]*/).then(seq(id, lparen.then(elem.many()).skip(rparen)))

const root = block.many()

export default {
  preserve(string) {
    let value = ''
    let level = 0
    string.split(/\r\n|\r|\n/).forEach((line) => {
      const indent = Math.floor(line.match(/^ */)[0].length / 2)
      if (level < indent) {
        value += "{".repeat(indent - level) + "\n" + line + "\n"
      } else if (level > indent) {
        value += "}".repeat(level - indent) + "\n" + line + "\n"
      } else {
        value += line + "\n"
      }
      level = indent
    })
    value += "}".repeat(level)
    return value
  },
  parse(string) {
    return root.parse(this.preserve(string)).value
  }
}

文書構造の定義

jsの前半部、constの並ぶ箇所は文書構造の定義です。
意味としては下記のような感じです。
elemとblockが相互参照して再帰しているのがわかるかと思います。

const elem = lazy('', () => { return block.or(line) })

elemはblockまたはlineで構成される

const block = regex(/[\n ]*/).then(seq(id, lparen.then(elem.many()).skip(rparen)))

blockはidで始まり、lparen{とrparen}で囲まれた複数個のelemで構成される

preserve

preserveは前処理です。

文書構造の定義でしれっと{}でブロックを定義していましたが、今回作りたいマークアップはインデントでネストを表現する方式なので、前処理でインデントを{}に変換しています。

ここもパーサでできればカッコいいのですが、うまいやり方は思いつきませんでした。pythonとかどうしてるんですかね?

配列からDOMを描画

vueで書きます。パース後の配列(persed)とバインドしておけば、リアルタイムに再描画されて楽です。

    div#view(:style="viewsize" ref="view")
      div(v-for="elem in parsed")

        div(v-if="check(elem, /^@mixings/)" :class="classname(elem)")
          table
            tr(v-for="mixing in take(elem)")
              td(v-for="i in columns(take(elem))") {{ mixing[i-1] || "" }}

        div(v-else-if="check(elem, /^@marks/)" :class="classname(elem)")
          div(v-for="mark in take(elem)")
            img(v-if="isValidMarkId(mark[0])" :src="`/img/${mark[0]}.jpg`")

        div(v-else-if="check(elem, /^@/)" :class="classname(elem)")
          div(v-for="e in take(elem)") {{ e.join(' ') }}

基本的には@hogeをそのままcssのclass(.hoge)として適用し、cssでデザインを定義していく戦略なので、あまり複雑なことはやりません。

ただし、@marksは画像(洗濯マーク)に差し替える必要があるのと、@mixingsはtableでレイアウトしたかったので、vue内で特別扱いしてあげます。

CSSの適用

CSSも書き直したらリアルタイムに反映されてほしいので、更新時に動的に差し替えに行きます。

    applyStyle() {
      const old = document.getElementById('inserted-style')
      old && old.parentNode.removeChild(old)
      const obj = document.createElement('style')
      obj.setAttribute('id', 'inserted-style')
      obj.appendChild(document.createTextNode(this.style))
      document.getElementsByTagName('head')[0].appendChild(obj)
    },

あまりキレイじゃないですが、head要素に無理やり差し込みます。

その他

HTMLからの画像化は、下記ライブラリを使用しています。
https://github.com/tsayen/dom-to-image
どうもフォントまわりの挙動が怪しく、OS/ブラウザによっては画像出力が上手くいかない場合があります。

まとめ

一通り作ってから、yamlでも良かったのでは、とちょっと思ってしまいました。

とはいえ、非エンジニアにはyamlも辛いでしょうし、目的特化して打鍵の少ない文法を定義したい、というところに独自マークアップの需要はあるかもしれません。WYSIWYGに発展できたりするといいですね。

ご参考になれば幸いです。