CSSのプロパティをソートするPerlスクリプト

CSSを書く時に「セレクタ内でCSS仕様書でのプロパティの出現順序に従ってソートする」という個人的なルールを守っている。何かコーディングにおいて便利な理由があるからというわけではなく、第三者に説明する時に「仕様書の出現順で書いてます!」とかで済ませられるから。今まではファイル全体を処理するオレオレPerlスクリプトで適当にやっていたのだけど、Vimで選択範囲だけをソートとかやりたくなったので、普通に標準入力を読んで結果を標準出力に吐くように書き直した。ついでにCSS3のプロパティとFirefox(Mozilla)やSafari(WebKit)、Opera(Presto)、Internet Explorer(Trident)の独自拡張などへも対応させたりとか。

#!/usr/bin/perl

# Author:   Kyo Nagashima <kyo@hail2u.net>, http://hail2u.net/
#           xaicron, http://blog.livedoor.jp/xaicron/
# License:  MIT license (http://opensource.org/licenses/mit-license.php)
# Modified: 2009-11-17T23:09:27+09:00

use strict;
use warnings;

# CSS2.1
my @property_order = qw(
margin
margin-top
margin-right
margin-bottom
margin-left
padding
padding-top
padding-right
padding-bottom
padding-left
border
border-top
border-bottom
border-right
border-left
border-width
border-top-width
border-right-width
border-bottom-width
border-left-width
border-color
border-top-color
border-right-color
border-bottom-color
border-left-color
border-style
border-top-style
border-right-style
border-bottom-style
border-left-style
display
position
top
right
bottom
left
float
clear
z-index
direction
unicode-bidi
width
min-width
max-width
height
min-height
max-height
line-height
vertical-align
overflow
clip
visibility
content
quotes
counter-reset
counter-increment
list-style
list-style-type
list-style-image
list-style-position
page-break-before
page-break-after
page-break-inside
orphans
widows
color
background
background-color
background-image
background-repeat
background-attachment
background-position
font
font-family
font-style
font-variant
font-weight
font-size
text-indent
text-align
text-decoration
text-shadow
letter-spacing
word-spacing
text-transform
white-space
caption-side
table-layout
border-collapse
border-spacing
empty-cells
cursor
outline
outline-width
outline-style
outline-color
volume
speak
pause-before
pause-after
pause
cue-before
cue-after
cue
play-during
azimuth
elevation
speech-rate
voice-family
pitch
pitch-range
stress
richness
speak-punctuation
speak-numeral
speak-header
);

# CSS3 and vendor extension for CSS3
push @property_order, qw(
background-clip
-moz-background-clip
-webkit-background-clip
background-origin
-moz-background-origin
-webkit-background-origin
background-size
-moz-background-size
-webkit-background-size
-o-background-size
border-radius
-moz-border-radius
-webkit-border-radius
border-top-right-radius
-moz-border-radius-topright
-moz-border-top-right-radius
border-bottom-right-radius
-moz-border-radius-bottomright
-moz-border-bottom-right-radius
border-bottom-left-radius
-moz-border-radius-bottomleft
-webkit-border-bottom-left-radius
border-top-left-radius
-moz-border-radius-topleft
-moz-border-top-left-radius
border-image
-moz-border-image
-webkit-border-image
border-image-source
border-image-slice
border-image-width
border-image-outset
border-image-repeat
box-break
box-shadow
-moz-box-shadow
-webkit-box-shadow
appearance
-moz-appearance
-webkit-appearance
icon
box-sizing
-moz-box-sizing
-webkit-box-sizing
outline-offset
resize
nav-index
overflow-x
-ms-overflow-x
overflow-y
-ms-overflow-y
overflow-style
-webkit-marquee
marquee-style
-webkit-marquee-style
marquee-loop
marquee-direction
-webkit-marquee-direction
marquee-speed
-webkit-marquee-speed
rotation
rotation-point
opacity
font-stretch
font-size-adjust
src
unicode-range
string-set
border-length
hyphens
hyphenate-resource
hyphenate-before
hyphenate-after
hyphenate-lines
hyphenate-character
text-replace
image-resolution
float-offset
marks
bookmark-level
bookmark-label
bookmark-target
target
target-name
target-new
target-position
text-height
line-stacking
line-stacking-strategy
line-stacking-ruby
line-stacking-shift
dominate-baseline
alignment-baseline
alignment-adjust
baseline-shift
inline-box-align
drop-initial-value
drop-initial-size
drop-initial-after-align
drop-initial-after-adjust
drop-initial-before-align
drop-initial-before-adjust
columns
-webkit-columns
column-width
-moz-column-width
-webkit-column-width
column-count
-moz-column-count
-webkit-column-count
column-gap
-moz-column-gap
-webkit-column-gap
column-rule
-moz-column-rule
-webkit-column-rule
column-rule-color
-moz-column-rule-color
-webkit-column-rule-color
column-rule-style
-moz-column-rule-style
-webkit-column-rule-style
column-rule-width
-moz-column-rule-width
-webkit-column-rule-width
column-span
column-fill
size
page
image-orientation
fit
fit-position
presentation-level
ruby-position
ruby-align
ruby-overhang
ruby-span
grid-columns
grid-rows
voice-volume
voice-balance
rest
rest-before
rest-after
mark
mark-before
mark-after
voice-rate
voice-pitch
voice-pitch-range
voice-stress
voice-duration
phonemes
white-space-collapse
word-break
-ms-word-break
text-wrap
word-wrap
-ms-word-wrap
text-align-last
-ms-text-align-last
text-justify
-ms-text-justify
punctuation-trim
text-emphasis
text-shadow
text-outline
text-indent
hanging-punctuation
box-orient
-moz-box-orient
-webkit-box-orient
box-direction
-moz-box-direction
-webkit-box-direction
box-original-group
-moz-box-ordinal-group
-webkit-box-ordinal-group
box-align
-moz-box-align
-webkit-box-align
box-flex
-moz-box-flex
-webkit-box-flex
box-flex-group
-moz-box-flexgroup
-webkit-box-flex-group
box-pack
-moz-box-pack
-webkit-box-pack
box-lines
-webkit-box-lines
transform
-moz-transform
-webkit-transform
transform-origin
-moz-transform-origin
-webkit-transform-origin
transform-style
-webkit-transform-style
perspective
-webkit-perspective
perspective-origin
-webkit-perspective-origin
backface-visibility
-webkit-backface-visibility
transition
-webkit-transition
transition-property
-webkit-transition-property
transition-duration
-webkit-transition-duration
transition-timing-function
-webkit-transition-timing-function
transition-delay
-webkit-transition-delay
animation
-webkit-animation
animation-name
-webkit-animation-name
animation-duration
-webkit-animation-duration
animation-timing-function
-webkit-animation-timing-function
animation-iteration-count
-webkit-animation-iteration-count
animation-direction
-webkit-animation-direction
animation-play-state
-webkit-animation-play-state
animation-delay
-webkit-animation-delay
);

# Vendor extension
push @property_order, qw(
-moz-background-inline-policy
-moz-binding
-moz-border-bottom-colors
-moz-border-left-colors
-moz-border-right-colors
-moz-border-top-colors
-moz-border-end
-moz-border-end-color
-moz-border-end-style
-moz-border-end-width
-moz-border-start
-moz-border-start-color
-moz-border-start-style
-moz-border-start-width
-moz-float-edge
-moz-force-broken-image-icon
-moz-image-region
-moz-margin-end
-moz-margin-start
-webkit-margin-start
-moz-outline-radius
-moz-outline-radius-bottomleft
-moz-outline-radius-bottomright
-moz-outline-radius-topleft
-moz-outline-radius-topright
-moz-padding-end
-moz-padding-start
-webkit-padding-start
-moz-stack-sizing
-moz-user-focus
-moz-user-input
-moz-user-modify
-webkit-user-modify
-moz-user-select
-webkit-user-select
-moz-window-shadow
-webkit-background-composite
-webkit-border-horizontal-spacing
-webkit-border-vertical-spacing
-webkit-box-reflect
-webkit-column-break-after
-webkit-column-break-before
-webkit-column-break-inside
-webkit-dashboard-region
-webkit-line-break
-webkit-margin-bottom-collapse
-webkit-margin-collapse
-webkit-margin-top-collapse
-webkit-marquee-increment
-webkit-marquee-repetition
-webkit-mask
-webkit-mask-attachment
-webkit-mask-box-image
-webkit-mask-clip
-webkit-mask-composite
-webkit-mask-image
-webkit-mask-origin
-webkit-mask-position
-webkit-mask-position-x
-webkit-mask-position-y
-webkit-mask-repeat
-webkit-mask-size
-webkit-nbsp-mode
-webkit-rtl-ordering
-webkit-tap-highlight-color
-webkit-text-fill-color
-webkit-text-security
-webkit-text-size-adjust
-webkit-text-stroke
-webkit-text-stroke-color
-webkit-text-stroke-width
-webkit-touch-callout
-webkit-transform-origin-x
-webkit-transform-origin-y
-webkit-transform-origin-z
-webkit-user-drag
-o-table-baseline
-ms-background-position-x
-ms-background-position-y
-ms-filter
-ms-ime-mode
-ms-layout-flow
-ms-layout-grid
-ms-layout-grid-char
-ms-layout-grid-line
-ms-layout-grid-mode
-ms-layout-grid-type
-ms-text-autospace
-ms-text-kashida-space
-ms-text-overflow
-ms-text-underline-position
-ms-writing-mode
-ms-interpolation-mode
-ms-scrollbar-3dlight-color
-ms-scrollbar-arrow-color
-ms-scrollbar-base-color
-ms-scrollbar-darkshadow-color
-ms-scrollbar-face-color
-ms-scrollbar-highlight-color
-ms-scrollbar-shadow-color
-ms-zoom
);

# Other CSS extensions (without vendor prefix)
push @property_order, qw(
image-rendering
ime-mode
pointer-events
text-rendering
);

# Create hash: key => property, value => index
# See also: http://blog.livedoor.jp/xaicron/archives/50913074.html
my %property = do {
  my $i = 0;
  map { $_ => $i++ } @property_order;
};

my $css = join "", <STDIN>;
$css =~ s/{(.*?)}/"{".sort_properties($1)."}"/imgse;

print $css;

sub sort_properties {
  # Remove leading line-breaks
  (my $rules = shift) =~ s/^\n+//;

  # Orcish Maneuver
  # my %temp;
  # my @sorted_rules = sort {
  #   ($temp{$a} ||= check_order($a)) <=> ($temp{$b} ||= check_order($b))
  # } split "\n", $rules;

  # Schwartzian Transform
  # See also: http://gist.github.com/236860
  my @sorted_rules
    = map  { $_->[0] }
      sort { $a->[1] <=> $b->[1] }
      map  { [ $_, check_order($_) ] }
      split "\n", $rules;

  return "\n" . join("\n", @sorted_rules) . "\n";
}

sub check_order {
  my $line = shift;
  my $order = 10000;

  if ($line =~ /^\s*(.+?)\s*:/) {
    if (defined $property{$1}) {
      $order = $property{$1};
    }
  # } else {
  #   warn "Unknown property: $1";
  }

  return $order;
}

安定のCSS2.1のプロパティが優先的に上になる。それに続いてCSS3の草案にあるプロパティがCSS Modulesの順に並ぶ。各ブラウザの独自拡張でCSS3の草案にそれに対応するものがある場合(border-radius-moz-border-radius-webkit-border-radiusなど)はその対応するものに続けて並べるようにした。それらに続いてプリフィックスのある各ブラウザの独自拡張プロパティ。一番下に来るのはプリフィックスの無い独自拡張プロパティでime-mode等がこれに当る。

まとめると「CSS2.1 > CSS3 > 独自拡張 > プリフィックスの無い独自拡張」という順序で並ぶことになり、具体的には以下のように並ぶ。

div#header ul#navigation li a {
  /* CSS2.1 */
  margin-left: 6px;
  padding: 0 6px;
  display: block;
  width: auto;
  height: 24px;
  line-height: 24px;
  color: #c9c6c0;
  background-image: url("../images/bg-navigation.png");
  background-repeat: repeat-x;
  /* CSS3 */
  border-radius: 3px;
  -moz-border-radius: 3px;
  -webkit-border-radius: 3px;
  /* Vendor extension */
  -moz-margin-start: 6px;
  /* Vendor extension (without prefix) */
  ime-mode: auto;
}

スクリプトではそれぞれを分けて順にpushしているので新規追加や修正はそれほど大変でもない(と思う)。

CSSのプロパティについては以下を参考にした。

「プロパティの位置がおかしい!」とか「プロパティに抜けがある!」とか「プロパティがかぶっている!」とかは報告してくれるとありがたい


Vimでこのスクリプトを利用する場合は、パスの通ったところにsortcss.plという名前で保存し、セレクタ全体もしくはファイル全体を選択して、

!sortcss.pl

とすれば良い。セレクタ全体を選択(正確には中括弧の間を中括弧を含めて選択)するにはビジュアル・モードでのa}を使えば良いので、以下のようなキーバインドとコマンドを用意しておくと便利。

nmap gso va}:SortCSS<CR>
vmap gso a}:SortCSS<CR>

" Sort CSS properties
" :SortCSS
command! -range=% SortCSS :<line1>,<line2>!perl -S sortcss.pl

これでgsoでソートできる他、:SortCSSでもソートできる(-range=%なので選択していない場合はファイル全体を処理する)。CSSファイル以外ではアクティブにする必要が無いので、$MYVIMRCではなく$HOME/.vim/ftplugin/css.vim(Windowsなら$HOME\vimfiles\ftplugin\css.vim)とかに書いた方が良いかも


プロパティの羅列部分だけを切り出せばCSS用の辞書に早変わり!

追記@2009-11-16T15:31:03+09:00

xaicronさん添削してもらった。なるほどなーということでコピペして更新。

追記@2009-11-16T18:28:19+09:00

重複していたspeak-headerの削除(kitsさんのはてブコメントより)とempty-cellscursorの位置が逆だったのを修正した。

追記@2009-11-17T23:15:06+09:00

xaicronさんからdefined抜けの修正(marginで場合によってはおかしくなることがあった)とシュワルツ変換を噛ませて高速化したものをフィードバックしてもらったので、手元で加えていた未知のプロパティを最後に回す修正とマージして変数名をちょっと変え更新。warnでの未知のプロパティの警告はあったほうが良いのだけど、Vimと一部のシェルの組み合わせでは標準エラー出力もリダイレクトするように(デフォルトでは)なっているのでコメントアウトしておいた。