TECH BOX

Technology blog from Web Engineer

この記事は最終更新日から3年以上経過しているため正確ではないかもしれません

Stylusの独自拡張関数

Stylusではファイル内にfunctionを使うこともできますが、コンパイル時に独自の拡張関数を作ることができます。

自分がよく使うのが画像のサイズを取得したい時や、Gulpでコンパイルする時のbackground-imageのURLを自動的に行いたいので独自拡張をしています。

ワークスペースはこのようになっていることを前提に記述していきます。

/_ws/
  └ /common/
      ├ /img/
      └ /stylus/

stylusに追加する独自関数

stylusにはコンパイルオプションとしてdefineというパラメータがあります。
これには独自の関数を追加したりすることができます。

// gulpの場合
import gulp from 'gulp'
import stylus from 'gulp-stylus'

const imgPath = ()=>{}
const imgUrl = ()=>{}


gulp.task('stylus', ()=>{
  gulp.src(['_ws/common/stylus/**/[!_]*.styl'])
    .pipe(stylus({
      define: {
        'path': imgPath,
        'image-url': imgUrl
      }
    }))
    .pipe(...)
})


// webpack 2の場合
export default {
  context: '***',
  output: {...}
  module: {
    rules: [
      {
        test: /^(?!_).*\.styl$/,
        use: [
          loader: 'stylus-loader',
          options: {
            define: {
              'path': imgPath,
              'image-url': imgUrl
            }
          }
        ]
      }
    ]
  }
}

これでstylus内でpath('...')image-url('...')の関数を使えるようになります。
下記ではそれぞれの関数を説明します。

画像パスを扱う共通処理

どの階層のファイルでも画像のパスを取得する方法。
Node.jsのpathとプロジェクトにインストールしているstylusを呼び出します。

import path from 'path'
import Stylus from 'stylus'

Stylusのnodeを呼び出しつつ、stylusのあるディレクトリから画像のディレクトリまでの初期指定を行います。

let Nodes = Stylus.nodes
const IMG_ROOT = '../img'

画像パスの取得ロジック

画像パスを検索するための処理を行います。

const imageUrlPath = (filename, image_name)=>{
  let arr = filename.split('/')
  arr.reverse()   // 配列を逆順にする
  arr.shift()     // 最初の値(ファイル名)を削除

  let idx;

  for(let i = 0, iLen = arr.length; i < iLen; i++){
    if(arr[i] === 'stylus'){
      idx = i;
      break;
    }
  }

  let roots = ''
  while(idx > 0){
    roots += '../'
    idx--
  }

  return path.join(roots + IMG_ROOT, image_name)
}

filenameにはコンパイルするstylusファイルのフルパスを渡します。
image_nameは画像名です。

ディレクトリ名のstylusに一致するまで回します。
一致したらディレクトリの階層をwhile文で決定します。

例えばコンパイルするファイルがstylus/style.stylだった場合は、一番最初にヒットするのでidxは0になりroots変数は空のままです。
しかし、stylus/foo/bar.stylがコンパイルされる場合はidxは1となりroots../が入ります。

最終的にパスとファイル名を結合。

画像パスを求める関数

stylus側で実行させる為の関数を作成。
stylusで$img = path('***')と呼び出せる。

const imgPath = urlObj=>{
  let img = imageUrlPath(urlObj.filename, urlObj.string)
  return new Nodes.String(img)
}

/stylus/style.styl$img = path('foo.png')と書いた場合にはコンパイル時に下記のオブジェクトが渡されます。

String {
  lineno: 14,
  column: 16,
  filename: '/path/to/_ws/common/stylus/style.styl',
  val: 'foo.png',
  string: 'foo.png',
  prefixed: false,
  quote: '\'' }

$img = path('foo/bar.png')と書くとval,stringにはfoo/bar.pngが渡されます。
filenameはコンパイルを実行しているファイルになります。

下記はいずれもstylus/style.stylのフルパスが渡ります。

// foo/_bar.styl
$img = path('bar.png')
// style.styl
@import "./foo/_bar.styl"
$img = path('foo/bar.png')

background-image用の関数

stylusでsassの様にbackground-image: image-url('***')と使えるようにする関数

const imgUrl = urlObj=>{
  let img = imageUrlPath(urlObj.filename, urlObj.string)
  return new Nodes.Literal(`url(${img}?${Date.now()})`)
}

これでstylusでも使えます。

// style.styl
.hoge
  background-image: image-url('foo.png')

// ↓コンパイル後
.hoge {
  background-image: url(../foo.png?123456);
}

?以降はキャッシュ対策のためコンパイル時にUNIXタイムスタンプを付与しています。

全ソース

import path from 'path'
import Stylus from 'stylus'

let Nodes = Stylus.nodes
const IMG_ROOT = '../img'

// 画像パスの解決
const imageUrlPath = (filename, image_name)=>{
  let arr = filename.split('/')
  arr.reverse()   // 配列を逆順にする
  arr.shift()     // 最初の値(ファイル名)を削除

  let idx;

  for(let i = 0, iLen = arr.length; i < iLen; i++){
    if(arr[i] === 'stylus'){
      idx = i;
      break;
    }
  }

  let roots = ''
  while(idx > 0){
    roots += '../'
    idx--
  }

  return path.join(roots + IMG_ROOT, image_name)
}

// pathの利用
const imgPath = urlObj=>{
  let img = imageUrlPath(urlObj.filename, urlObj.string)
  return new Nodes.String(img)
}


// image-urlの利用
const imgUrl = urlObj=>{
  let img = imageUrlPath(urlObj.filename, urlObj.string)
  return new Nodes.Literal(`url(${img}?${Date.now()})`)
}


// 別ファイルにしてエクスポートする場合
export default {
  'path': imgPath,
  'image-url': imgUrl
}

※Javascript Standard Styleルールで記述しています