html.ex (7450B)
1 defmodule Makeup.Styles.HTML do 2 defmodule Style do 3 defstruct long_name: "", 4 short_name: "", 5 background_color: "#ffffff", 6 highlight_color: "#ffffcc", 7 styles: [] 8 9 alias Makeup.Styles.HTML.TokenStyle 10 require Makeup.Token.Utils 11 alias Makeup.Token.Utils 12 13 defp handle_inheritance(style_map) do 14 # Handles insheritance between styles. 15 # This is automatic in Pygments' design, because they use class inheritance for tokens. 16 # We don't have class inheritance in elixir, so we must have something else. 17 # Here, we use a manually build hierarchy to fake inheritance. 18 # 19 # In any case, the goal is to have flat tokens at runtime. 20 # This function is only called at compile time. 21 Enum.reduce(Utils.precedence(), style_map, fn {parent_key, child_keys}, style_map -> 22 parent_style = style_map[parent_key] 23 24 Enum.reduce(child_keys, style_map, fn child_key, style_map -> 25 child_style = style_map[child_key] 26 27 Map.put( 28 style_map, 29 child_key, 30 Map.merge( 31 parent_style, 32 child_style, 33 fn _k, v1, v2 -> v2 || v1 end 34 ) 35 ) 36 end) 37 end) 38 end 39 40 require EEx 41 42 EEx.function_from_string( 43 :def, 44 :render_css, 45 """ 46 .<%= highlight_class %> .hll {background-color: <%= highlight_color %>} 47 .<%= highlight_class %> {\ 48 <%= if token_text.color do %>color: <%= token_text.color %>; <% end %>\ 49 <%= if token_text.font_style do %>font-style: <%= token_text.font_style %>; <% end %>\ 50 <%= if token_text.font_weight do %>font-weight: <%= token_text.font_weight %>; <% end %>\ 51 <%= if token_text.border do %>border: <%= token_text.border %>; <% end %>\ 52 <%= if token_text.text_decoration do %>text-decoration: <%= token_text.text_decoration %>; <% end %>\ 53 <%= if background_color do %>background-color: <%= background_color %><% end %>}\ 54 .<%= highlight_class %> .unselectable { 55 -webkit-touch-callout: none; 56 -webkit-user-select: none; 57 -khtml-user-select: none; 58 -moz-user-select: none; 59 -ms-user-select: none; 60 user-select: none; 61 } 62 <%= for {css_class, token_style, token_type} <- styles do %> 63 .<%= highlight_class %> .<%= css_class %> {\ 64 <%= if token_style.color do %>color: <%= token_style.color %>; <% end %>\ 65 <%= if token_style.font_style do %>font-style: <%= token_style.font_style %>; <% end %>\ 66 <%= if token_style.font_weight do %>font-weight: <%= token_style.font_weight %>; <% end %>\ 67 <%= if token_style.border do %>border: <%= token_style.border %>; <% end %>\ 68 <%= if token_style.text_decoration do %>text-decoration: <%= token_style.text_decoration %>; <% end %>\ 69 <%= if token_style.background_color do %>background-color: <%= token_style.background_color %>; <% end %>\ 70 } /* :<%= Atom.to_string(token_type) %> */\ 71 <% end %> 72 """, 73 [:highlight_class, :highlight_color, :background_color, :token_text, :styles] 74 ) 75 76 @doc """ 77 Generate a stylesheet for a style. 78 """ 79 def stylesheet(style, css_class \\ "highlight") do 80 token_styles = 81 style.styles 82 |> Map.delete(:text) 83 |> Enum.into([]) 84 |> Enum.map(fn {token_type, token_style} -> 85 css_class = Makeup.Token.Utils.css_class_for_token_type(token_type) 86 {css_class, token_style, token_type} 87 end) 88 |> Enum.filter(fn {_, token_style, _} -> 89 Makeup.Styles.HTML.TokenStyle.not_empty?(token_style) 90 end) 91 |> Enum.sort() 92 93 token_text = style.styles[:text] 94 95 render_css( 96 css_class, 97 style.highlight_color, 98 style.background_color, 99 token_text, 100 token_styles 101 ) 102 end 103 104 @doc """ 105 Creates a new style. 106 107 Takes care of unspecified token types and inheritance. 108 Writes and caches a CSS stylesheet for the style. 109 """ 110 def make_style(options \\ []) do 111 short_name = Keyword.fetch!(options, :short_name) 112 long_name = Keyword.fetch!(options, :long_name) 113 background_color = Keyword.fetch!(options, :background_color) 114 highlight_color = Keyword.fetch!(options, :highlight_color) 115 incomplete_style_map = Keyword.fetch!(options, :styles) 116 117 complete_style_map = 118 Utils.standard_token_types() 119 |> Enum.map(fn k -> {k, ""} end) 120 |> Enum.into(%{}) 121 |> Map.merge(incomplete_style_map) 122 |> Enum.map(fn {k, v} -> {k, TokenStyle.from_string(v)} end) 123 |> Enum.into(%{}) 124 |> handle_inheritance 125 126 %__MODULE__{ 127 long_name: long_name, 128 short_name: short_name, 129 background_color: background_color, 130 highlight_color: highlight_color, 131 styles: complete_style_map 132 } 133 end 134 end 135 136 defmodule TokenStyle do 137 @moduledoc """ 138 A CSS style for a single token. 139 """ 140 141 defstruct font_style: nil, 142 font_weight: nil, 143 border: nil, 144 text_decoration: nil, 145 color: nil, 146 background_color: nil, 147 literal: nil 148 149 @doc """ 150 A `TokenStyle` is considered empty if all its fields are `nil`. 151 152 A CSS class for an empty `TokenStyle` is not rendered in the stylesheet. 153 This saves a little space and makes the stylesheet more human-readable. 154 """ 155 def empty?(style) do 156 not not_empty?(style) 157 end 158 159 @doc """ 160 A `TokenStyle` is empty if at least a field is not `nil`. 161 162 A CSS class for an empty `TokenStyle` is rendered in the stylesheet. 163 """ 164 def not_empty?(style) do 165 style |> Map.from_struct() |> Map.values() |> Enum.any?() 166 end 167 168 # Foreground color 169 defp to_attr("#" <> _ = color), do: {:color, color} 170 # Background color 171 defp to_attr("bg:" <> color), do: {:background_color, color} 172 # Border (can only specify border color) 173 defp to_attr("border:" <> color), do: {:border, color} 174 # Font weight (bold vs normal) 175 defp to_attr("bold"), do: {:font_weight, "bold"} 176 defp to_attr("nobold"), do: {:font_weight, "normal"} 177 # Font style (italic vs oblique vs normal) 178 defp to_attr("italic"), do: {:font_style, "italic"} 179 defp to_attr("oblique"), do: {:font_style, "oblique"} 180 defp to_attr("noitalic"), do: {:font_style, "normal"} 181 # Text decoration (underline vs none) 182 defp to_attr("underline"), do: {:text_decoration, "underline"} 183 # Unrecognized commands: 184 defp to_attr(other) do 185 # Log the command 186 IO.warn("unknown attribute #{inspect(other)}") 187 false 188 end 189 190 @doc """ 191 Creates a `TokenStyle` from string description. 192 193 The string description is highly optimized for the goal of being typed by a human. 194 The following commands are recognized: 195 196 * `~r/#[0-9a-f]+/` for foreround color 197 * `~r/bg:#[0-9a-f]+/` for background color 198 * `~r/border:#[0-9a-f]+/` for border color 199 * `italic` for `font-style: italic` 200 * `oblique` for `font-style: oblique` 201 * `noitalic` for `font-style: normal` 202 * `underline` for `font-style: underline` 203 204 No other commands are currently recognized. 205 """ 206 def from_string(str) do 207 attrs = 208 str 209 |> String.split() 210 |> Enum.map(&to_attr/1) 211 |> Enum.filter(fn x -> x end) 212 213 struct(TokenStyle, attrs) 214 end 215 end 216 end