2018年11月20日火曜日
独自マークアップ言語によるケアラベル(品質表示ネーム)エディター
こんにちは、シタテルの茨木です。
衣服には必ずケアラベル(品質表示ネーム)というものがついています。洗濯の方法などが書いてあるアレですね。
凝ったデザインにすることもありますが、大体はパターンが決まっています。
今回は、独自のマークアップ言語でケアラベルを作れるようにしてみた、という内容です。
完成イメージ
- 中央の入力欄で独自マークアップを入力すると、左にケアラベルがリアルタイムプレビューされる
- 画像としてケアラベルをダウンロードできる
- 右欄でCSSによるデザイン微調整が可能(おまけ)
ソースコード
https://github.com/tibaraki/care-label
目次
- マークアップをパースして配列に
- 配列からDOMを描画
- 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に発展できたりするといいですね。
ご参考になれば幸いです。