@@ -119,7 +119,7 @@ defmodule Phoenix.HTML do
119119 iex> html_escape("<hello>")
120120 {:safe, [[[] | "<"], "hello" | ">"]}
121121
122- iex> html_escape(' <hello>' )
122+ iex> html_escape(~c" <hello>" )
123123 {:safe, ["<", 104, 101, 108, 108, 111, ">"]}
124124
125125 iex> html_escape(1)
@@ -337,4 +337,75 @@ defmodule Phoenix.HTML do
337337 do: javascript_escape ( t , << acc :: binary , h >> )
338338
339339 defp javascript_escape ( << >> , acc ) , do: acc
340+
341+ @ doc """
342+ Escapes a string for use as a CSS identifier.
343+
344+ ## Examples
345+
346+ iex> css_escape("hello world")
347+ "hello\\ \\ world"
348+
349+ iex> css_escape("-123")
350+ "-\\ \\ 31 23"
351+
352+ """
353+ @ spec css_escape ( String . t ( ) ) :: String . t ( )
354+ def css_escape ( value ) when is_binary ( value ) do
355+ # This is a direct translation of
356+ # https://github.com/mathiasbynens/CSS.escape/blob/master/css.escape.js
357+ # into Elixir.
358+ value
359+ |> String . to_charlist ( )
360+ |> escape_css_chars ( )
361+ |> IO . iodata_to_binary ( )
362+ end
363+
364+ defp escape_css_chars ( chars ) do
365+ case chars do
366+ # If the character is the first character and is a `-` (U+002D), and
367+ # there is no second character, […]
368+ [ ?- | [ ] ] -> [ "\\ -" ]
369+ _ -> escape_css_chars ( chars , 0 , [ ] )
370+ end
371+ end
372+
373+ defp escape_css_chars ( [ ] , _ , acc ) , do: Enum . reverse ( acc )
374+
375+ defp escape_css_chars ( [ char | rest ] , index , acc ) do
376+ escaped =
377+ cond do
378+ # If the character is NULL (U+0000), then the REPLACEMENT CHARACTER
379+ # (U+FFFD).
380+ char == 0 ->
381+ << 0xFFFD :: utf8 >>
382+
383+ # If the character is in the range [\1-\1F] (U+0001 to U+001F) or is
384+ # U+007F,
385+ # if the character is the first character and is in the range [0-9]
386+ # (U+0030 to U+0039),
387+ # if the character is the second character and is in the range [0-9]
388+ # (U+0030 to U+0039) and the first character is a `-` (U+002D),
389+ char in 0x0001 .. 0x001F or char == 0x007F or
390+ ( index == 0 and char in ?0 .. ?9 ) or
391+ ( index == 1 and char in ?0 .. ?9 and hd ( acc ) == "-" ) ->
392+ # https://drafts.csswg.org/cssom/#escape-a-character-as-code-point
393+ [ "\\ " , Integer . to_string ( char , 16 ) , " " ]
394+
395+ # If the character is not handled by one of the above rules and is
396+ # greater than or equal to U+0080, is `-` (U+002D) or `_` (U+005F), or
397+ # is in one of the ranges [0-9] (U+0030 to U+0039), [A-Z] (U+0041 to
398+ # U+005A), or [a-z] (U+0061 to U+007A), […]
399+ char >= 0x0080 or char in [ ?- , ?_ ] or char in ?0 .. ?9 or char in ?A .. ?Z or char in ?a .. ?z ->
400+ # the character itself
401+ << char :: utf8 >>
402+
403+ true ->
404+ # Otherwise, the escaped character.
405+ # https://drafts.csswg.org/cssom/#escape-a-character
406+ [ "\\ " , << char :: utf8 >> ]
407+ end
408+
409+ escape_css_chars ( rest , index + 1 , [ escaped | acc ] )
410+ end
340411end
0 commit comments