├── test ├── test_helper.exs ├── files_for_tests │ ├── example_code3.ex │ ├── example_code1.ex │ ├── example_code2.ex │ └── example_code4.ex └── duplex_test.exs ├── duplex ├── .gitignore ├── doc ├── fonts │ ├── icomoon.eot │ ├── icomoon.ttf │ ├── icomoon.woff │ └── icomoon.svg ├── .build ├── index.html ├── dist │ ├── sidebar_items-b8960a7b6d.js │ └── app-a07cea761b.css ├── 404.html ├── api-reference.html ├── Mix.Tasks.Scan.html └── Duplex.html ├── config └── config.exs ├── README.md ├── mix.exs ├── mix.lock ├── LICENSE └── lib └── duplex.ex /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /duplex: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zirkonit/duplex/HEAD/duplex -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | ./duplex -------------------------------------------------------------------------------- /doc/fonts/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zirkonit/duplex/HEAD/doc/fonts/icomoon.eot -------------------------------------------------------------------------------- /doc/fonts/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zirkonit/duplex/HEAD/doc/fonts/icomoon.ttf -------------------------------------------------------------------------------- /doc/fonts/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zirkonit/duplex/HEAD/doc/fonts/icomoon.woff -------------------------------------------------------------------------------- /test/files_for_tests/example_code3.ex: -------------------------------------------------------------------------------- 1 | defmodule M3 do 2 | @moduledoc """ 3 | M3 4 | """ 5 | def f(n, m) do 6 | n + m 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /doc/.build: -------------------------------------------------------------------------------- 1 | dist/app-4aef34ad5f.js 2 | dist/app-a07cea761b.css 3 | fonts/icomoon.eot 4 | fonts/icomoon.svg 5 | fonts/icomoon.ttf 6 | fonts/icomoon.woff 7 | dist/sidebar_items-b8960a7b6d.js 8 | api-reference.html 9 | index.html 10 | 404.html 11 | Duplex.html 12 | Mix.Tasks.Scan.html 13 | -------------------------------------------------------------------------------- /test/files_for_tests/example_code1.ex: -------------------------------------------------------------------------------- 1 | defmodule M1 do 2 | @moduledoc """ 3 | M1 4 | """ 5 | def f(a, b) do 6 | if b != 0 do 7 | if a > b do 8 | a = a - b 9 | else 10 | b = b - a 11 | end 12 | f(a, b) 13 | else 14 | a 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/files_for_tests/example_code2.ex: -------------------------------------------------------------------------------- 1 | defmodule M2 do 2 | @moduledoc """ 3 | M2 4 | """ 5 | def f(n, m) do 6 | if m != 0 do 7 | if n > m do 8 | n = n - m 9 | else 10 | m = m - n 11 | end 12 | f(n, m) 13 | else 14 | n 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | duplex v0.1.1 – Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /test/files_for_tests/example_code4.ex: -------------------------------------------------------------------------------- 1 | 2 | defmodule M4 do 3 | @moduledoc """ 4 | M4 5 | """ 6 | def f(a, b) do 7 | if b != 0 do 8 | if a > b do 9 | a = a - b 10 | else 11 | b = b - a 12 | # 1 13 | end 14 | f(a, b) 15 | else 16 | a 17 | # 2 18 | end 19 | # 3 20 | end 21 | # 4 22 | end 23 | -------------------------------------------------------------------------------- /doc/dist/sidebar_items-b8960a7b6d.js: -------------------------------------------------------------------------------- 1 | sidebarNodes={"extras":[{"id":"api-reference","title":"API Reference","group":"","headers":[{"id":"Modules","anchor":"modules"}]}],"exceptions":[],"modules":[{"id":"Duplex","title":"Duplex","functions":[{"id":"code_blocks/3","anchor":"code_blocks/3"},{"id":"compare_nodes/4","anchor":"compare_nodes/4"},{"id":"equal_code/2","anchor":"equal_code/2"},{"id":"get_ast/1","anchor":"get_ast/1"},{"id":"get_files/1","anchor":"get_files/1"},{"id":"get_ranges_for_jobs/2","anchor":"get_ranges_for_jobs/2"},{"id":"get_shape/1","anchor":"get_shape/1"},{"id":"is_equal/2","anchor":"is_equal/2"},{"id":"read_files/4","anchor":"read_files/4"},{"id":"show_similar/3","anchor":"show_similar/3"}]},{"id":"Mix.Tasks.Scan","title":"Mix.Tasks.Scan","functions":[{"id":"run/1","anchor":"run/1"}]}],"protocols":[]} -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :duplex, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:duplex, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Duplex 2 | 3 | ## Description 4 | 5 | Duplex allows you to search for similar code blocks inside your Elixir project. 6 | 7 | ## Installation as escript 8 | Remotely (without repository cloning) 9 | ``` 10 | mix escript.install https://raw.githubusercontent.com/zirkonit/duplex/master/duplex 11 | ``` 12 | 13 | Locally 14 | ``` 15 | mix do escript.build, escript.install 16 | ``` 17 | 18 | ## Usage as escript 19 | ``` 20 | cd /path/to/project 21 | ~/.mix/escripts/duplex 22 | ``` 23 | 24 | ## Installation as dependency 25 | 26 | 1. Add `:duplex` to deps in `mix.exs` 27 | 28 | ```elixir 29 | def deps do 30 | [{:duplex, "~> 0.1.1"}] 31 | end 32 | ``` 33 | 2. Update dependencies 34 | 35 | ``` 36 | mix deps.get 37 | ``` 38 | 39 | ## Usage as dependency 40 | 41 | ```elixir 42 | iex -S mix 43 | Duplex.show_similar 44 | ``` 45 | 46 | ## Config 47 | 48 | You can change default values on `config.exs` by adding next lines with your own values 49 | 50 | ```elixir 51 | config :duplex, threshold: 7 # filter AST nodes with `node.length + node.depth >= threshold` 52 | # Than lower threshold, than simpler nodes will be included. 53 | # Optimal value is around 7-10. Default is 7. 54 | config :duplex, dirs: ["lib", "config", "web"] # directories to search for Elixir source files 55 | config :duplex, n_jobs: 4 # number of threads 56 | ``` 57 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Duplex.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :duplex, 6 | version: "0.1.1", 7 | elixir: "~> 1.3", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | description: description(), 11 | package: package(), 12 | test_coverage: [tool: Coverex.Task], 13 | deps: deps(), 14 | escript: escript()] 15 | end 16 | 17 | def application do 18 | [applications: [:logger]] 19 | end 20 | 21 | def escript do 22 | [main_module: Duplex] 23 | end 24 | 25 | defp deps do 26 | [ 27 | {:dogma, "~> 0.1", only: :dev}, 28 | {:dir_walker, ">= 0.0.0"}, 29 | # {:exprof, "~> 0.2.0"}, 30 | {:ex_doc, ">= 0.0.0", only: :dev}, 31 | {:credo, "~> 0.5", only: [:dev, :test]}, 32 | {:coverex, "~> 1.4.10", only: :test} 33 | ] 34 | end 35 | 36 | defp description do 37 | """ 38 | Duplex allows you to search for similar code blocks inside your project. 39 | 40 | ## Usage 41 | As escript 42 | ``` 43 | cd /path/to/project 44 | ~/.mix/escripts/duplex 45 | ``` 46 | or as dependency 47 | ```elixir 48 | iex -S mix 49 | Duplex.show_similar 50 | ``` 51 | """ 52 | end 53 | 54 | defp package do 55 | [ 56 | name: :duplex, 57 | maintainers: ["Ivan Cherevko", "Andrew Koryagin"], 58 | licenses: ["Apache 2.0"], 59 | links: %{}, 60 | ] 61 | end 62 | 63 | end 64 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, 2 | "certifi": {:hex, :certifi, "0.7.0", "861a57f3808f7eb0c2d1802afeaae0fa5de813b0df0979153cbafcd853ababaf", [:rebar3], []}, 3 | "coverex": {:hex, :coverex, "1.4.12", "b18d737734edeac578a854cfaa5aa48c5d05e649c77ce02efb7f723a316b6a3b", [:mix], [{:hackney, "~> 1.5", [hex: :hackney, optional: false]}, {:poison, "~> 1.5 or ~> 2.0", [hex: :poison, optional: false]}]}, 4 | "credo": {:hex, :credo, "0.5.3", "0c405b36e7651245a8ed63c09e2d52c2e2b89b6d02b1570c4d611e0fcbecf4a2", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, 5 | "dir_walker": {:hex, :dir_walker, "0.0.6", "d068e4306bce63d7475e38efabd0e27429153ce10546774b7789a9a9fa40154d", [:mix], []}, 6 | "dogma": {:hex, :dogma, "0.1.13", "7b6c6ad2b3ee6501eda3bd39e197dd5198be8d520d1c175c7f713803683cf27a", [:mix], [{:poison, ">= 2.0.0", [hex: :poison, optional: false]}]}, 7 | "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, 8 | "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 9 | "hackney": {:hex, :hackney, "1.6.5", "8c025ee397ac94a184b0743c73b33b96465e85f90a02e210e86df6cbafaa5065", [:rebar3], [{:certifi, "0.7.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.1", [hex: :ssl_verify_fun, optional: false]}]}, 10 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 11 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 12 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 13 | "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, 14 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.1", "28a4d65b7f59893bc2c7de786dec1e1555bd742d336043fe644ae956c3497fbe", [:make, :rebar], []}} 15 | -------------------------------------------------------------------------------- /doc/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 404 – duplex v0.1.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 22 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |

Page not found

63 | 64 |

Sorry, but the page you were trying to get to, does not exist. You 65 | may want to try searching this site using the sidebar or using our 66 | API Reference page to find what 67 | you were looking for.

68 | 69 | 82 |
83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /doc/api-reference.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | API Reference – duplex v0.1.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 22 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |

63 | duplex v0.1.1 64 | API Reference 65 |

66 | 67 | 68 |
69 |

70 | 71 | Modules 72 |

73 | 74 |
75 |
76 | 77 | 78 |

Duplex allows you to search for similar code blocks inside your Elixir project

79 |
80 | 81 |
82 |
83 | 84 | 85 |
86 | 87 |
88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | 108 |
109 |
110 |
111 |
112 | 113 | 114 | 115 | 116 | -------------------------------------------------------------------------------- /test/duplex_test.exs: -------------------------------------------------------------------------------- 1 | defmodule DuplexTest do 2 | use ExUnit.Case 3 | doctest Duplex 4 | import Duplex 5 | 6 | test "getting all .ex, .exs files" do 7 | assert Duplex.get_files("test") == ["test/test_helper.exs", 8 | "test/files_for_tests/example_code4.ex", 9 | "test/files_for_tests/example_code3.ex", 10 | "test/files_for_tests/example_code2.ex", 11 | "test/files_for_tests/example_code1.ex", 12 | "test/duplex_test.exs"] 13 | assert Duplex.get_files("path_does_not_exist") == [] 14 | end 15 | 16 | def get_nodes(threshold, f_index \\ nil) do 17 | dir = "test/files_for_tests/" 18 | name = "example_code" 19 | ext = ".ex" 20 | f_names = ["#{dir}#{name}1#{ext}", 21 | "#{dir}#{name}2#{ext}", 22 | "#{dir}#{name}3#{ext}", 23 | "#{dir}#{name}4#{ext}"] 24 | files = if f_index do 25 | Enum.slice(f_names, f_index..f_index) 26 | else 27 | f_names 28 | end 29 | nodes = for file <- files do 30 | code_blocks(file, threshold) 31 | end 32 | nodes |> Enum.flat_map(&(&1)) 33 | end 34 | 35 | test "example_code duplicates" do 36 | results = [[{"test/files_for_tests/example_code1.ex", {1, 14}, 13}, 37 | {"test/files_for_tests/example_code2.ex", {1, 14}, 13}, 38 | {"test/files_for_tests/example_code4.ex", {2, 16}, 13}]] 39 | f_name = "test_res.txt" 40 | dir = ["test/files_for_tests"] 41 | assert Duplex.show_similar(dir, nil, nil, f_name) == results 42 | assert Duplex.show_similar(dir, 20, nil, f_name) == results 43 | assert File.rm(f_name) == :ok 44 | end 45 | 46 | test "async works the same" do 47 | f_name = "test_res.txt" 48 | dir = ["test/files_for_tests"] 49 | four_th = Duplex.show_similar(dir, nil, 4, f_name) 50 | one_th = Duplex.show_similar(dir, nil, 1, f_name) 51 | assert four_th == one_th 52 | assert File.rm(f_name) == :ok 53 | end 54 | 55 | test "async file reading" do 56 | dir = "test/files_for_tests/" 57 | name = "example_code" 58 | ext = ".ex" 59 | files = ["#{dir}#{name}1#{ext}", 60 | "#{dir}#{name}2#{ext}", 61 | "#{dir}#{name}3#{ext}", 62 | "#{dir}#{name}4#{ext}"] 63 | chunks = for i <- 1..8 do 64 | Duplex.read_files(files, i, 7) 65 | end 66 | assert chunks |> Enum.uniq |> length == 1 67 | end 68 | 69 | test "shape hashes" do 70 | nodes1 = get_nodes(7, 0) 71 | nodes2 = get_nodes(7, 1) 72 | nodes3 = get_nodes(7, 2) 73 | {_, shape1} = nodes1 |> Enum.max_by(fn {_, shape} -> shape[:depth] end) 74 | {_, shape2} = nodes2 |> Enum.max_by(fn {_, shape} -> shape[:depth] end) 75 | {_, shape3} = nodes3 |> Enum.max_by(fn {_, shape} -> shape[:depth] end) 76 | h1 = shape1 |> Duplex.hash_shape 77 | h2 = shape2 |> Duplex.hash_shape 78 | h3 = shape3 |> Duplex.hash_shape 79 | assert h1 == h2 80 | assert h1 != h3 81 | end 82 | 83 | test "argparse" do 84 | args = ["--help", "--njobs", "10", 85 | "--threshold", "2", "--file", 86 | "/path/to/file.ex"] 87 | assert Duplex.parse_args(args) == {true, 2, 10, "/path/to/file.ex"} 88 | end 89 | 90 | test "this app has no duplicates" do 91 | assert Duplex.main(["lib"]) == [] 92 | end 93 | 94 | test "escript" do 95 | assert Duplex.main(["--help"]) == Duplex.help_text 96 | assert Duplex.main == Duplex.show_similar 97 | end 98 | 99 | test "writting file" do 100 | f_name = "f.ex" 101 | assert Duplex.write_file(f_name, ["data1", "data2"]) == :ok 102 | assert File.read!(f_name) == "data1\ndata2\n" 103 | assert File.rm(f_name) == :ok 104 | end 105 | 106 | end 107 | -------------------------------------------------------------------------------- /doc/fonts/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /doc/Mix.Tasks.Scan.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Mix.Tasks.Scan – duplex v0.1.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 22 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |

63 | duplex v0.1.1 64 | Mix.Tasks.Scan 65 | 66 | 67 |

68 | 69 | 70 | 71 | 72 |
73 |

74 | 75 | 76 | 77 | Summary 78 |

79 | 80 | 81 | 82 |
83 |

84 | Functions 85 |

86 |
87 |
88 | run() 89 |
90 | 91 |

A task needs to implement run which receives 92 | a list of command line args

93 |
94 | 95 |
96 | 97 |
98 | 99 | 100 | 101 | 102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 |
111 |

112 | 113 | 114 | 115 | Functions 116 |

117 |
118 | 119 |
120 | 121 | 122 | 123 | run() 124 | 125 | 126 | 127 |
128 |
129 |

A task needs to implement run which receives 130 | a list of command line args.

131 |

Callback implementation for Mix.Task.run/1.

132 | 133 |
134 |
135 | 136 |
137 | 138 | 139 | 140 | 141 | 142 | 155 |
156 |
157 |
158 |
159 | 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /doc/Duplex.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Duplex – duplex v0.1.1 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 22 | 56 | 57 |
58 |
59 |
60 | 61 | 62 |

63 | duplex v0.1.1 64 | Duplex 65 | 66 | 67 |

68 | 69 | 70 |
71 |

Duplex allows you to search for similar code blocks inside your Elixir project.

72 | 73 |
74 | 75 | 76 | 77 |
78 |

79 | 80 | 81 | 82 | Summary 83 |

84 | 85 | 86 | 87 |
88 |

89 | Functions 90 |

91 |
92 | 95 | 96 |
97 |
98 | 101 | 102 |
103 |
104 | 107 | 108 |
109 |
110 |
111 | get_ast(filename) 112 |
113 | 114 |
115 |
116 |
117 | get_files(dir) 118 |
119 | 120 |
121 |
122 | 125 | 126 |
127 |
128 |
129 | get_shape(tnode) 130 |
131 | 132 |
133 |
134 |
135 | is_equal(s1, s2) 136 |
137 | 138 |
139 |
140 | 143 | 144 |
145 | 151 | 152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 |
160 | 161 | 162 | 163 | 164 | 165 |
166 |

167 | 168 | 169 | 170 | Functions 171 |

172 |
173 | 174 |
175 | 176 | 177 | 178 | code_blocks(file, min_depth, min_length) 179 | 180 | 181 | 182 |
183 |
184 | 185 |
186 |
187 |
188 | 189 |
190 | 191 | 192 | 193 | compare_nodes(nodes, len_nodes, from, until) 194 | 195 | 196 | 197 |
198 |
199 | 200 |
201 |
202 |
203 | 204 |
205 | 206 | 207 | 208 | equal_code(nodes, n_jobs) 209 | 210 | 211 | 212 |
213 |
214 | 215 |
216 |
217 |
218 | 219 |
220 | 221 | 222 | 223 | get_ast(filename) 224 | 225 | 226 | 227 |
228 |
229 | 230 |
231 |
232 |
233 | 234 |
235 | 236 | 237 | 238 | get_files(dir) 239 | 240 | 241 | 242 |
243 |
244 | 245 |
246 |
247 |
248 | 249 |
250 | 251 | 252 | 253 | get_ranges_for_jobs(len, n_jobs) 254 | 255 | 256 | 257 |
258 |
259 | 260 |
261 |
262 |
263 | 264 |
265 | 266 | 267 | 268 | get_shape(tnode) 269 | 270 | 271 | 272 |
273 |
274 | 275 |
276 |
277 |
278 | 279 |
280 | 281 | 282 | 283 | is_equal(s1, s2) 284 | 285 | 286 | 287 |
288 |
289 | 290 |
291 |
292 |
293 | 294 |
295 | 296 | 297 | 298 | read_files(files, n_jobs, min_depth, min_length) 299 | 300 | 301 | 302 |
303 |
304 | 305 |
306 |
307 |
308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 |
316 | 317 | 318 | 319 | show_similar(dirs \\ ["lib"], n_jobs \\ nil, export_file \\ nil) 320 | 321 | 322 | 323 |
324 |
325 | 326 |
327 |
328 | 329 |
330 | 331 | 332 | 333 | 334 | 335 | 348 |
349 |
350 |
351 |
352 | 353 | 354 | 355 | 356 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /lib/duplex.ex: -------------------------------------------------------------------------------- 1 | defmodule Duplex do 2 | 3 | @moduledoc """ 4 | Duplex allows you to search for similar code blocks inside your Elixir 5 | project. 6 | """ 7 | 8 | def help_text do 9 | """ 10 | Duplex escript usage: 11 | 12 | --help - show this message 13 | --threshold - filter AST nodes by `node.length + node.depth >= threshold`. 14 | Than lower threshold, than simpler nodes will be included. 15 | Optimal value is around 7-10. Default is 7. 16 | --njobs - number of threads will be used for AST parsing 17 | --file - file to export output 18 | """ 19 | end 20 | 21 | def parse_args(args) do 22 | {args, _, _} = OptionParser.parse(args) 23 | args = args |> Enum.into(%{}) 24 | extract = fn args, key, to_int -> 25 | if key in Map.keys(args) do 26 | if to_int do 27 | {parsed, _} = Integer.parse(args[key]) 28 | parsed 29 | else 30 | args[key] 31 | end 32 | else 33 | nil 34 | end 35 | end 36 | help = extract.(args, :help, false) 37 | threshold = extract.(args, :threshold, true) 38 | n_jobs = extract.(args, :njobs, true) 39 | export_file = extract.(args, :file, false) 40 | {help, threshold, n_jobs, export_file} 41 | end 42 | 43 | def main(args \\ []) do 44 | {help, threshold, n_jobs, export_file} = parse_args(args) 45 | if help do 46 | IO.puts help_text 47 | help_text 48 | else 49 | Duplex.show_similar(nil, threshold, n_jobs, export_file) 50 | end 51 | end 52 | 53 | defp flatten(e) do 54 | Enum.flat_map(e, &(&1)) 55 | end 56 | 57 | # Recursively scan directory to find elixir source files 58 | def get_files(dir) do 59 | if File.exists?(dir) do 60 | {:ok, walker} = DirWalker.start_link(dir) 61 | walker |> DirWalker.next(100_000_000) |> Enum.filter(fn item -> 62 | ext = item |> String.split(".") |> Enum.reverse |> hd 63 | (ext == "ex") or (ext == "exs") 64 | end) 65 | else 66 | [] 67 | end 68 | end 69 | 70 | def get_ast(filename) do 71 | try do 72 | Code.string_to_quoted!(File.read!(filename)) 73 | rescue 74 | _ -> 75 | nil 76 | end 77 | end 78 | 79 | # Recursive AST tree search to get all it's nodes 80 | defp visit(tnode, nodes) do 81 | nodes = nodes ++ [tnode] 82 | ch = children(tnode) 83 | if ch do 84 | new_nodes = for c <- ch do 85 | {_, nodes} = visit(c, nodes) 86 | nodes 87 | end 88 | nodes = new_nodes |> flatten 89 | {ch, nodes} 90 | else 91 | {[], nodes} 92 | end 93 | end 94 | 95 | defp filter_nodes(nodes) do 96 | nodes |> Enum.uniq |> Enum.filter(fn item -> 97 | case item do 98 | {_, _, nil} -> 99 | false 100 | {_, _, c} when is_list(c) -> 101 | true 102 | {_, _, _} -> 103 | false 104 | nil -> 105 | false 106 | _ -> 107 | !(Keyword.keyword?(item)) and is_tuple(item) 108 | end 109 | end) 110 | end 111 | 112 | # Get all informative nodes from AST tree 113 | def code_blocks(file, threshold) do 114 | {_, nodes} = visit(get_ast(file), []) 115 | nodes = filter_nodes(nodes) 116 | # calculate shapes only once 117 | nodes = for n <- nodes, do: {{n, file}, get_shape(n)} 118 | # filter short blocks, non deep blocks 119 | nodes = nodes |> Enum.filter(fn x -> valid_line_numbers?(x) end) 120 | nodes = nodes |> Enum.filter(fn x -> deep_long?(x, threshold) end) 121 | nodes = nodes |> Enum.reverse |> Enum.uniq_by(fn {_, s} -> s[:lines] end) 122 | nodes |> Enum.reverse 123 | end 124 | 125 | defp valid_line_numbers?({_, %{lines: {min, max}, depth: _}}) do 126 | (max != nil) and (min != nil) 127 | end 128 | 129 | defp deep_long?({_, %{lines: {min, max}, depth: depth}}, threshold) do 130 | len = max - min + 1 131 | len + depth >= threshold 132 | end 133 | 134 | defp read_content(map, files) do 135 | if length(files) > 0 do 136 | file = hd(files) 137 | map = map |> Map.put(file, file |> File.read! |> String.split("\n")) 138 | read_content(map, tl(files)) 139 | else 140 | map 141 | end 142 | end 143 | 144 | defp get_configs(dirs, threshold, n_jobs) do 145 | d_dirs = ["lib", "config", "web"] 146 | {d_threshold, d_n_jobs} = {7, 4} 147 | 148 | c_dirs = Application.get_env(:duplex, :dirs) 149 | c_threshold = Application.get_env(:duplex, :threshold) 150 | c_n_jobs = Application.get_env(:duplex, :n_jobs) 151 | choose_one = fn i, j, k -> 152 | if i, do: i, else: if j, do: j, else: k 153 | end 154 | dirs = choose_one.(dirs, c_dirs, d_dirs) 155 | threshold = choose_one.(threshold, c_threshold, d_threshold) 156 | n_jobs = choose_one.(n_jobs, c_n_jobs, d_n_jobs) 157 | 158 | {dirs, threshold, n_jobs} 159 | end 160 | 161 | defp read_by_chunk(chunks, threshold) do 162 | tasks = for files <- chunks do 163 | Task.async(fn -> 164 | nodes = for file <- files do 165 | code_blocks(file, threshold) 166 | end 167 | nodes = nodes |> flatten 168 | nodes 169 | end) 170 | end 171 | nodes = for task <- tasks do 172 | Task.await(task, timeout) 173 | end 174 | nodes = nodes |> flatten 175 | nodes 176 | end 177 | 178 | def read_files(files, n_jobs, threshold) do 179 | n_jobs = if n_jobs <= 0, do: 1, else: n_jobs 180 | n_jobs = if length(files) < n_jobs, do: length(files), else: n_jobs 181 | size = div(length(files), n_jobs) 182 | chunks = for n <- 0..(n_jobs - 1) do 183 | cond do 184 | n == 0 -> 185 | Enum.slice(files, 0, size) 186 | n == n_jobs - 1 -> 187 | Enum.slice(files, n * size, 2 * size) 188 | true -> 189 | Enum.slice(files, n * size, size) 190 | end 191 | end 192 | read_by_chunk(chunks, threshold) 193 | end 194 | 195 | defp get_additional_lines(current, min, max, content) do 196 | f = fn i, acc -> 197 | if i == " " |> to_charlist |> hd do 198 | {:cont, acc + 1} 199 | else 200 | {:halt, acc} 201 | end 202 | end 203 | first = current |> hd |> to_charlist |> Enum.reduce_while(0, f) 204 | last = current |> Enum.reverse |> hd |> to_charlist 205 | last = last |> Enum.reduce_while(0, f) 206 | if first != last do 207 | item = content |> Enum.slice(max + 1..max + 1) |> hd 208 | if String.trim(item) == "end" do 209 | spaces = item |> to_charlist |> Enum.reduce_while(0, f) 210 | if spaces >= first do 211 | get_additional_lines(current ++ [item], min, max + 1, content) 212 | else 213 | {current, min, max} 214 | end 215 | else 216 | {current, min, max} 217 | end 218 | else 219 | {current, min, max} 220 | end 221 | end 222 | 223 | defp get_output_data(groups, content) do 224 | space = ["---------------------------------------------------------------"] 225 | # get data to show 226 | all_data = for group <- groups do 227 | gr = for item <- group |> Enum.reverse do 228 | {file, {min, max}, _} = item 229 | min = min - 1 230 | max = max - 1 231 | c = Enum.slice(content[file], min..max) 232 | {c, min, max} = get_additional_lines(c, min, max, content[file]) 233 | c = Enum.zip(min + 1..max + 1, c) 234 | c = for l <- c do 235 | {number, line} = l 236 | "#{number}: #{line}" 237 | end 238 | ["#{file}:"] ++ c ++ [""] 239 | end 240 | gr = gr |> flatten 241 | gr ++ space 242 | end 243 | all_data |> flatten 244 | end 245 | 246 | # Main function to find equal code parts. 247 | # dirs - directories to scan for elixir source files 248 | # export_file - if not nil, write results to the file by this path 249 | def show_similar(dirs \\ nil, t_hold \\ nil, n_jobs \\ nil, export \\ nil) do 250 | configs = get_configs(dirs, t_hold, n_jobs) 251 | {dirs, t_hold, n_jobs} = configs 252 | # scan dirs 253 | files = for d <- dirs do 254 | get_files(d) 255 | end 256 | files = files |> flatten 257 | IO.puts "Reading files..." 258 | nodes = read_files(files, n_jobs, t_hold) 259 | # get map of file contents (key, balue = filename, content) 260 | content = read_content(Map.new(), files) 261 | IO.puts "Searching for duplicates..." 262 | # get grouped equal code blocks 263 | groups = nodes |> equal_code 264 | all_data = groups |> get_output_data(content) 265 | nothing_found = "There are no duplicates" 266 | if export == nil do 267 | for item <- all_data do 268 | IO.puts item 269 | end 270 | if length(all_data) == 0 do 271 | IO.puts nothing_found 272 | end 273 | else 274 | all_data = if length(all_data) == 0 do 275 | [nothing_found] 276 | else 277 | all_data 278 | end 279 | write_file(export, all_data) 280 | end 281 | groups 282 | end 283 | 284 | def write_file(filename, data) do 285 | try do 286 | {:ok, file} = File.open(filename, [:write]) 287 | for d <- data do 288 | IO.binwrite(file, "#{d}\n") 289 | end 290 | File.close(file) 291 | rescue 292 | _ -> 293 | :error 294 | end 295 | end 296 | 297 | defp timeout do 298 | 1_000 * 60 * 30 299 | end 300 | 301 | def hash_map(nodes, map \\ %{}) do 302 | if length(nodes) > 0 do 303 | {{n, file}, s} = nodes |> hd 304 | hash = hash_shape(s) 305 | map = if hash in Map.keys(map) do 306 | Map.put(map, hash, map[hash] ++ [{file, s[:lines], s[:depth]}]) 307 | else 308 | Map.put(map, hash, [{file, s[:lines], s[:depth]}]) 309 | end 310 | hash_map(nodes |> tl, map) 311 | else 312 | map 313 | end 314 | end 315 | 316 | def hash_shape(shape, hash \\ "") do 317 | tmp = if shape[:variable], do: "var", else: inspect(shape[:name]) 318 | current = "#{hash}#{tmp}#{length(shape[:children])}" 319 | if length(shape[:children]) > 0 do 320 | ch_hashes = for ch <- shape[:children] do 321 | hash_shape(ch) 322 | end 323 | current <> (ch_hashes |> Enum.join("")) 324 | else 325 | current 326 | end 327 | end 328 | 329 | # Find equal code parts 330 | def equal_code(nodes) do 331 | groups = nodes |> hash_map 332 | # keep only groups with size > 1 333 | hashes = groups |> Map.keys 334 | single_nodes = hashes |> Enum.filter(fn hash -> 335 | length(groups[hash]) == 1 336 | end) 337 | groups = Map.drop(groups, single_nodes) 338 | # filter subsamples 339 | keys = Map.keys(groups) 340 | not_subsamples = Enum.filter(keys, fn hash -> 341 | not subsample?(hash, keys) 342 | end) 343 | groups = for hash <- not_subsamples, do: groups[hash] 344 | groups = for gr <- groups do 345 | gr |> Enum.sort_by(fn {file, {min, _}, _} -> 346 | {file, -min} 347 | end) 348 | end 349 | groups |> Enum.sort_by(fn gr -> 350 | {_, {min, max}, depth} = gr |> hd 351 | - (max - min + depth) * length(gr) 352 | end) 353 | end 354 | 355 | defp subsample?(hash, keys) do 356 | tmp = for key <- keys do 357 | String.length(hash) < String.length(key) and String.contains?(key, hash) 358 | end 359 | Enum.any?(tmp) 360 | end 361 | 362 | # Get children of the node 363 | defp children(tnode) do 364 | case tnode do 365 | {_, _, nodes} -> 366 | nodes 367 | _ -> 368 | cond do 369 | Keyword.keyword?(tnode) -> 370 | tnode |> Enum.into(%{}) |> Map.values 371 | is_list(tnode) -> 372 | tnode 373 | true -> 374 | nil 375 | end 376 | end 377 | end 378 | 379 | # Get structured shape of the node for comparison 380 | def get_shape(tnode) do 381 | ch = children(tnode) 382 | ch = if ch do 383 | for c <- ch do 384 | get_shape(c) 385 | end 386 | else 387 | [] 388 | end 389 | depth = if length(ch) == 0 do 390 | 1 391 | else 392 | Enum.max_by(ch, fn item -> item[:depth] end)[:depth] + 1 393 | end 394 | case tnode do 395 | {name, _, content} -> 396 | # is node a variable? 397 | var = (content == nil) 398 | name = if is_atom(name) do 399 | name 400 | else 401 | "_" 402 | end 403 | %{name: name, 404 | lines: get_lines(tnode), 405 | children: ch, 406 | variable: var, 407 | depth: depth} 408 | _ -> 409 | if is_integer(tnode) or is_float(tnode) do 410 | %{name: tnode, 411 | lines: get_lines(tnode), 412 | children: ch, 413 | variable: false, 414 | depth: depth} 415 | else 416 | %{name: "_", 417 | lines: get_lines(tnode), 418 | children: ch, 419 | variable: false, 420 | depth: depth} 421 | end 422 | end 423 | end 424 | 425 | # Recursively go through the node and finds {min, max} line numbers 426 | defp get_lines(tnode, main \\ true) do 427 | current = case tnode do 428 | {_, data, _} -> 429 | case data do 430 | [line: l] -> 431 | l 432 | [counter: _, line: l] -> 433 | l 434 | _ -> 435 | nil 436 | end 437 | _ -> 438 | nil 439 | end 440 | ch = children(tnode) 441 | from_ch = if ch do 442 | for c <- ch do 443 | get_lines(c, false) 444 | end 445 | else 446 | [] 447 | end 448 | from_ch = from_ch |> flatten 449 | current = Enum.filter(from_ch, fn item -> item != nil end) ++ [current] 450 | current = Enum.uniq(current) 451 | if main do 452 | {Enum.min(current), Enum.max(current)} 453 | else 454 | current 455 | end 456 | end 457 | 458 | end 459 | -------------------------------------------------------------------------------- /doc/dist/app-a07cea761b.css: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Lato:300,700|Merriweather:300italic,300|Inconsolata:400,700);.hljs,article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}img,legend{border:0}.results ul,.sidebar ul{list-style:none}.night-mode-toggle:focus,.sidebar .sidebar-search .sidebar-searchInput:focus,.sidebar .sidebar-search .sidebar-searchInput:hover,.sidebar-toggle:active,.sidebar-toggle:focus,.sidebar-toggle:hover,a:active,a:hover{outline:0}.hljs-comment{color:#8e908c}.css .hljs-class,.css .hljs-id,.css .hljs-pseudo,.hljs-attribute,.hljs-regexp,.hljs-tag,.hljs-variable,.html .hljs-doctype,.ruby .hljs-constant,.xml .hljs-doctype,.xml .hljs-pi,.xml .hljs-tag .hljs-title{color:#c82829}.hljs-built_in,.hljs-constant,.hljs-literal,.hljs-number,.hljs-params,.hljs-pragma,.hljs-preprocessor{color:#f5871f}.css .hljs-rule .hljs-attribute,.ruby .hljs-class .hljs-title{color:#eab700}.hljs-header,.hljs-inheritance,.hljs-name,.hljs-string,.hljs-value,.ruby .hljs-symbol,.xml .hljs-cdata{color:#718c00}.css .hljs-hexcolor,.hljs-title{color:#3e999f}.coffeescript .hljs-title,.hljs-function,.javascript .hljs-title,.perl .hljs-sub,.python .hljs-decorator,.python .hljs-title,.ruby .hljs-function .hljs-title,.ruby .hljs-title .hljs-keyword{color:#4271ae}.hljs-keyword,.javascript .hljs-function{color:#8959a8}.hljs{overflow-x:auto;background:#fff;color:#4d4d4c;padding:.5em;-webkit-text-size-adjust:none}legend,td,th{padding:0}.coffeescript .javascript,.javascript .xml,.tex .hljs-formula,.xml .css,.xml .hljs-cdata,.xml .javascript,.xml .vbscript{opacity:.5}/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}.content-outer,body{background-color:#fff}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}table{border-collapse:collapse;border-spacing:0}@font-face{font-family:icomoon;src:url(../fonts/icomoon.eot?h5z89e);src:url(../fonts/icomoon.eot?#iefixh5z89e) format('embedded-opentype'),url(../fonts/icomoon.ttf?h5z89e) format('truetype'),url(../fonts/icomoon.woff?h5z89e) format('woff'),url(../fonts/icomoon.svg?h5z89e#icomoon) format('svg');font-weight:400;font-style:normal}.icon-elem,[class*=" icon-"],[class^=icon-]{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.sidebar,body{font-family:Lato,sans-serif}.icon-link:before{content:"\e005"}.icon-search:before{content:"\e036"}.icon-cross:before{content:"\e117"}@media screen and (max-width:768px){.icon-menu{font-size:1em}}@media screen and (min-width:769px){.icon-menu{font-size:1.25em}}@media screen and (min-width:1281px){.icon-menu{font-size:1.5em}}.icon-menu:before{content:"\e120"}.icon-angle-right:before{content:"\f105"}.icon-code:before{content:"\f121"}body,html{box-sizing:border-box;height:100%;width:100%}body{margin:0;font-size:16px;line-height:1.6875em}*,:after,:before{box-sizing:inherit}.main{display:-ms-flexbox;display:-ms-flex;display:flex;-ms-flex-pack:end;justify-content:flex-end}.sidebar{display:-ms-flexbox;display:-ms-flex;display:flex;-webkit-box-orient:vertical;-moz-box-orient:vertical;-webkit-box-direction:normal;-moz-box-direction:normal;min-height:0;-moz-flex-direction:column;-ms-flex-direction:column;flex-direction:column;width:300px;height:100%;position:fixed;top:0;left:0;z-index:4}.content{width:100%;padding-left:300px;overflow-y:auto;-webkit-overflow-scrolling:touch;height:100%;position:relative;z-index:3}@media screen and (max-width:768px){body .content{z-index:0;padding-left:0}body .sidebar{z-index:3;transform:translateX(-102%);will-change:transform}}body.sidebar-closed .sidebar,body.sidebar-closing .sidebar,body.sidebar-opening .sidebar{z-index:0}body.sidebar-opened .sidebar-toggle,body.sidebar-opening .sidebar-toggle{transform:translateX(250px)}@media screen and (max-width:768px){body.sidebar-opened .sidebar,body.sidebar-opening .sidebar{transform:translateX(0)}}body.sidebar-closed .content,body.sidebar-closing .content{padding-left:0}body.sidebar-closed .sidebar-toggle,body.sidebar-closing .sidebar-toggle{transform:none}body.sidebar-closed .icon-menu{color:#000}.sidebar-toggle i,.sidebar-toggle:hover{color:#e1e1e1}body.sidebar-opening .sidebar-toggle{transition:transform .3s ease-in-out}body.sidebar-opening .content{padding-left:300px;transition:padding-left .3s ease-in-out}@media screen and (max-width:768px){body.sidebar-opening .content{padding-left:0}body.sidebar-opening .sidebar{transition:transform .3s ease-in-out;z-index:3}}body.sidebar-closing .sidebar-toggle{transition:transform .3s ease-in-out}body.sidebar-closing .content{transition:padding-left .3s ease-in-out}@media screen and (max-width:768px){body.sidebar-closing .sidebar{z-index:3;transition:transform .3s ease-in-out;transform:translateX(-102%)}}.sidebar a,.sidebar-toggle{transition:color .3s ease-in-out}body.sidebar-closed .sidebar{visibility:hidden}.content-inner{max-width:949px;margin:0 auto;padding:3px 60px}.content-outer{min-height:100%}@media screen and (max-width:768px){.content-inner{padding:27px 20px 27px 40px}}.sidebar-toggle{position:fixed;z-index:99;left:18px;top:8px;background-color:transparent;border:none;padding:0;font-size:16px;will-change:transform;transform:translateX(250px)}@media screen and (max-width:768px){.sidebar-toggle{transform:translateX(0);left:5px;top:5px}.sidebar-opened .sidebar-toggle{left:18px;top:5px}}.sidebar{font-size:15px;line-height:18px;background:#373f52;color:#d5dae6;overflow:hidden}.sidebar .gradient{background:linear-gradient(#373f52,rgba(55,63,82,0));height:20px;margin-top:-20px;pointer-events:none;position:relative;top:20px;z-index:100}.sidebar ul li{margin:0;padding:0 10px}.sidebar a{color:#d5dae6;text-decoration:none}.sidebar a:hover{color:#fff}.sidebar .sidebar-projectLink{margin:23px 30px 0}.sidebar .sidebar-projectDetails{display:inline-block;text-align:right;vertical-align:top;margin-top:6px}.sidebar .sidebar-projectImage{display:inline-block;max-width:64px;max-height:64px;margin-left:15px;vertical-align:bottom}.sidebar .sidebar-projectName{font-weight:700;font-size:24px;line-height:30px;color:#fff;margin:0;padding:0;max-width:230px;word-break:break-all}.sidebar .sidebar-projectVersion{margin:0;padding:0;font-weight:300;font-size:16px;line-height:20px;color:#fff}.sidebar .sidebar-listNav{padding:10px 30px 20px;margin:0}.sidebar .sidebar-listNav li,.sidebar .sidebar-listNav li a{text-transform:uppercase;font-weight:300;font-size:14px}.sidebar .sidebar-listNav li{padding-left:17px;border-left:3px solid transparent;transition:all .3s linear;line-height:27px}.sidebar .sidebar-listNav li.selected,.sidebar .sidebar-listNav li.selected a,.sidebar .sidebar-listNav li:hover,.sidebar .sidebar-listNav li:hover a{border-color:#9768d1;color:#fff}.sidebar .sidebar-search{margin:23px 30px 18px;display:-ms-flexbox;display:-ms-flex;display:flex}.sidebar .sidebar-search i.icon-search{font-size:14px;color:#d5dae6}.sidebar .sidebar-search .sidebar-searchInput{background-color:transparent;border:none;border-radius:0;border-bottom:1px solid #959595;margin-left:5px;height:20px}.sidebar #full-list{margin:0 0 0 30px;padding:10px 20px 40px;overflow-y:auto;-webkit-overflow-scrolling:touch;-moz-flex:1 1 .01%;-ms-flex:1 1 .01%;flex:1 1 .01%;-ms-flex-positive:1;-ms-flex-negative:1;-ms-flex-preferred-size:.01%}.sidebar #full-list ul{display:none;margin:9px 15px;padding:0}.sidebar #full-list ul li{font-weight:300;line-height:18px;padding:2px 10px}.sidebar #full-list ul li a.expand:before{content:"+";font-family:monospaced;font-size:15px;float:left;width:13px;margin-left:-13px}.sidebar #full-list ul li.open a.expand:before{content:"−"}.sidebar #full-list ul li ul{display:none;margin:9px 6px}.sidebar #full-list li.open>ul,.sidebar #full-list ul li.open>ul{display:block}.sidebar #full-list ul li ul li{border-left:1px solid #959595;padding:0 10px}.sidebar #full-list ul li ul li.active:before{font-family:icomoon;speak:none;font-style:normal;font-weight:400;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\f105";margin-left:-10px;font-size:16px;margin-right:5px}.sidebar #full-list li{padding:0;line-height:27px}.sidebar #full-list li.active{border-left:none}.sidebar #full-list li.active>a,.sidebar #full-list li.clicked>a{color:#fff}.sidebar #full-list li.group{text-transform:uppercase;font-weight:700;font-size:.8em;margin:2em 0 0;line-height:1.8em;color:#ddd}@media screen and (max-height:500px){.sidebar{overflow-y:auto}.sidebar #full-list{overflow:visible}}.content-inner{font-family:Merriweather,serif;font-size:1em;line-height:1.55em}.content-inner h1,.content-inner h2,.content-inner h3,.content-inner h4,.content-inner h5,.content-inner h6{font-family:Lato,sans-serif;font-weight:700;line-height:1.5em;word-wrap:break-word}.content-inner h1{font-size:2em;margin:1em 0 .5em}.content-inner h1.section-heading{margin:1.5em 0 .5em}.content-inner h1 small{font-weight:300}.content-inner h1 a.view-source{font-size:1.2rem}.content-inner h2{font-size:1.6em;margin:1em 0 .5em;font-weight:700}.content-inner h3{font-size:1.375em;margin:1em 0 .5em;font-weight:700}.content-inner a{color:#000;text-decoration:none;text-shadow:.03em 0 #fff,-.03em 0 #fff,0 .03em #fff,0 -.03em #fff,.06em 0 #fff,-.06em 0 #fff,.09em 0 #fff,-.09em 0 #fff,.12em 0 #fff,-.12em 0 #fff,.15em 0 #fff,-.15em 0 #fff;background-image:linear-gradient(#fff,#fff),linear-gradient(#fff,#fff),linear-gradient(#000,#000);background-size:.05em 1px,.05em 1px,1px 1px;background-repeat:no-repeat,no-repeat,repeat-x;background-position:0 90%,100% 90%,0 90%}.content-inner a:selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner a:-moz-selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner a *,.content-inner a :after,.content-inner a :before,.content-inner a:after,.content-inner a:before{text-shadow:none}.content-inner a:visited{color:#000}.content-inner ul li{line-height:1.5em}.content-inner ul li>p{margin:0}.content-inner a.view-source{float:right;color:#959595;background:0 0;border:none;text-shadow:none;transition:color .3s ease-in-out;margin-top:1px}.content-inner a.view-source:hover{color:#373f52}.content-inner .note{color:#959595;margin:0 5px;font-size:14px;font-weight:400}.content-inner blockquote{font-style:italic;margin:.5em 0;padding:.25em 1.5em;border-left:3px solid #e1e1e1;display:inline-block}.content-inner blockquote :first-child{padding-top:0;margin-top:0}.content-inner blockquote :last-child{padding-bottom:0;margin-bottom:0}.content-inner table{margin:2em 0}.content-inner th{text-align:left;font-family:Lato,sans-serif;text-transform:uppercase;font-weight:700;padding-bottom:.5em}.content-inner tr{border-bottom:1px solid #d5dae6;vertical-align:bottom;height:2.5em}.content-inner .summary .summary-row .summary-signature a,.content-inner .summary h2 a{background:0 0;border:none;text-shadow:none}.content-inner td,.content-inner th{padding-left:1em;line-height:2em}.content-inner .section-heading:hover a.hover-link{opacity:1;text-decoration:none}.content-inner .section-heading a.hover-link{transition:opacity .3s ease-in-out;display:inline-block;opacity:0;padding:.3em .6em .6em;line-height:1em;margin-left:-2.7em;background:0 0;border:none;text-shadow:none;font-size:16px;vertical-align:middle}.content-inner .detail h2.section-heading{margin:1.5em 0 .5em .3em}.content-inner .visible-xs{display:none!important}@media screen and (max-width:767px){.content-inner .visible-xs{display:block!important}}.content-inner img{max-width:100%}.content-inner .summary h2{font-weight:700}.content-inner .summary .summary-row .summary-signature{font-family:Inconsolata,Menlo,Courier,monospace;font-weight:700}.content-inner .summary .summary-row .summary-synopsis{font-family:Merriweather,serif;font-style:italic;padding:0 1.2em;margin:0 0 .5em}.content-inner .summary .summary-row .summary-synopsis p{margin:0;padding:0}.content-inner .detail-header{margin:2em 0 1em;padding:.7em 1em .5em;background:#f7f7f7;border-left:3px solid #9768d1;font-size:1em;font-family:Inconsolata,Menlo,Courier,monospace;position:relative}.content-inner .detail-header .note{float:right}.content-inner .detail-header .signature{font-size:1.2rem;font-weight:700}.content-inner .detail-header:hover a.detail-link{opacity:1;text-decoration:none}.content-inner .detail-header a.detail-link{transition:opacity .3s ease-in-out;position:absolute;top:0;left:0;display:block;opacity:0;padding:.6em;line-height:1.5em;margin-left:-2.5em;background:0 0;border:none;text-shadow:none}.content-inner .footer .line,.search-results h1{display:inline-block}.content-inner .specs pre,.content-inner code{font-family:Inconsolata,Menlo,Courier,monospace;font-style:normal;line-height:24px}.content-inner .specs{opacity:.7;padding-bottom:.05em}.content-inner .specs pre{font-size:.9em;margin:0;padding:0}.content-inner .docstring{margin:1.2em 0 2.1em 1.2em}.content-inner .docstring h2,.content-inner .docstring h3,.content-inner .docstring h4,.content-inner .docstring h5{font-weight:700}.content-inner .docstring h2{font-size:1em}.content-inner .docstring h3{font-size:.95em}.content-inner .docstring h4{font-size:.9em}.content-inner .docstring h5{font-size:.85em}.content-inner a.no-underline,.content-inner pre a{color:#9768d1;text-shadow:none;text-decoration:none;background-image:none}.content-inner a.no-underline:active,.content-inner a.no-underline:focus,.content-inner a.no-underline:hover,.content-inner a.no-underline:visited,.content-inner pre a:active,.content-inner pre a:focus,.content-inner pre a:hover,.content-inner pre a:visited{color:#9768d1;text-decoration:none}.content-inner code{font-weight:400;background-color:#f7f9fc;vertical-align:baseline;border-radius:2px;padding:.1em .2em}.content-inner pre{margin:1.5em 0}.content-inner pre.spec{margin:0}.content-inner pre.spec code{padding:0}.content-inner pre code.hljs{white-space:inherit;padding:.5em 1em;background-color:#f7f9fc}.content-inner .footer{margin:4em auto 1em;text-align:center;font-style:italic;font-size:14px;color:#959595}.content-inner .footer a{color:#959595;text-decoration:none;text-shadow:.03em 0 #fff,-.03em 0 #fff,0 .03em #fff,0 -.03em #fff,.06em 0 #fff,-.06em 0 #fff,.09em 0 #fff,-.09em 0 #fff,.12em 0 #fff,-.12em 0 #fff,.15em 0 #fff,-.15em 0 #fff;background-image:linear-gradient(#fff,#fff),linear-gradient(#fff,#fff),linear-gradient(#959595,#959595);background-size:.05em 1px,.05em 1px,1px 1px;background-repeat:no-repeat,no-repeat,repeat-x;background-position:0 90%,100% 90%,0 90%}.content-inner .footer a:selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.content-inner .footer a:-moz-selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}.results .result-id a,.search-results a.close-search{text-shadow:none;background-image:none;transition:color .3s ease-in-out}.content-inner .footer a *,.content-inner .footer a :after,.content-inner .footer a :before,.content-inner .footer a:after,.content-inner .footer a:before{text-shadow:none}.content-inner .footer a:visited{color:#959595}.search-results a.close-search{display:inline-block;float:right}.search-results a.close-search:active,.search-results a.close-search:focus,.search-results a.close-search:visited{color:#000}.search-results a.close-search:hover{color:#9768d1}.results .result-id{font-size:1.2em}.results .result-id a:active,.results .result-id a:focus,.results .result-id a:visited{color:#000}.results .result-id a:hover{color:#9768d1}.results .result-elem em,.results .result-id em{font-style:normal;color:#9768d1}.results ul{margin:0;padding:0}.night-mode-toggle{background:0 0;border:none}.night-mode-toggle:after{font-size:12px;content:'Switch to night mode';text-decoration:underline}body.night-mode{background:#212127}body.night-mode .hljs-comment{color:#969896}body.night-mode .css .hljs-class,body.night-mode .css .hljs-id,body.night-mode .css .hljs-pseudo,body.night-mode .hljs-attribute,body.night-mode .hljs-regexp,body.night-mode .hljs-tag,body.night-mode .hljs-variable,body.night-mode .html .hljs-doctype,body.night-mode .ruby .hljs-constant,body.night-mode .xml .hljs-doctype,body.night-mode .xml .hljs-pi,body.night-mode .xml .hljs-tag .hljs-title{color:#c66}body.night-mode .hljs-built_in,body.night-mode .hljs-constant,body.night-mode .hljs-literal,body.night-mode .hljs-number,body.night-mode .hljs-params,body.night-mode .hljs-pragma,body.night-mode .hljs-preprocessor{color:#de935f}body.night-mode .css .hljs-rule .hljs-attribute,body.night-mode .ruby .hljs-class .hljs-title{color:#f0c674}body.night-mode .hljs-header,body.night-mode .hljs-inheritance,body.night-mode .hljs-name,body.night-mode .hljs-string,body.night-mode .hljs-value,body.night-mode .ruby .hljs-symbol,body.night-mode .xml .hljs-cdata{color:#b5bd68}body.night-mode .css .hljs-hexcolor,body.night-mode .hljs-title{color:#8abeb7}body.night-mode .coffeescript .hljs-title,body.night-mode .hljs-function,body.night-mode .javascript .hljs-title,body.night-mode .perl .hljs-sub,body.night-mode .python .hljs-decorator,body.night-mode .python .hljs-title,body.night-mode .ruby .hljs-function .hljs-title,body.night-mode .ruby .hljs-title .hljs-keyword{color:#81a2be}body.night-mode .hljs-keyword,body.night-mode .javascript .hljs-function{color:#b294bb}body.night-mode .hljs{display:block;overflow-x:auto;background:#1d1f21;color:#c5c8c6;padding:.5em;-webkit-text-size-adjust:none}body.night-mode .coffeescript .javascript,body.night-mode .javascript .xml,body.night-mode .tex .hljs-formula,body.night-mode .xml .css,body.night-mode .xml .hljs-cdata,body.night-mode .xml .javascript,body.night-mode .xml .vbscript{opacity:.5}body.night-mode .content-outer{background:#212127}body.night-mode .night-mode-toggle:after{color:#959595;content:'Switch to day mode';text-decoration:underline}body.night-mode .close-search:active,body.night-mode .close-search:focus,body.night-mode .close-search:visited,body.night-mode .results .result-id a:active,body.night-mode .results .result-id a:focus,body.night-mode .results .result-id a:visited{color:#D2D2D2}body.night-mode .close-search:hover,body.night-mode .results .result-id a:hover{color:#9768d1}body.night-mode .content-inner{color:#B4B4B4}body.night-mode .content-inner h1,body.night-mode .content-inner h2,body.night-mode .content-inner h3,body.night-mode .content-inner h4,body.night-mode .content-inner h5,body.night-mode .content-inner h6{color:#D2D2D2}body.night-mode .content-inner a{color:#D2D2D2;text-decoration:none;text-shadow:.03em 0 #212127,-.03em 0 #212127,0 .03em #212127,0 -.03em #212127,.06em 0 #212127,-.06em 0 #212127,.09em 0 #212127,-.09em 0 #212127,.12em 0 #212127,-.12em 0 #212127,.15em 0 #212127,-.15em 0 #212127;background-image:linear-gradient(#212127,#212127),linear-gradient(#212127,#212127),linear-gradient(#D2D2D2,#D2D2D2);background-size:.05em 1px,.05em 1px,1px 1px;background-repeat:no-repeat,no-repeat,repeat-x;background-position:0 90%,100% 90%,0 90%}body.night-mode .content-inner a:selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}body.night-mode .content-inner a:-moz-selection{text-shadow:.03em 0 #b4d5fe,-.03em 0 #b4d5fe,0 .03em #b4d5fe,0 -.03em #b4d5fe,.06em 0 #b4d5fe,-.06em 0 #b4d5fe,.09em 0 #b4d5fe,-.09em 0 #b4d5fe,.12em 0 #b4d5fe,-.12em 0 #b4d5fe,.15em 0 #b4d5fe,-.15em 0 #b4d5fe;background:#b4d5fe}body.night-mode .content-inner a *,body.night-mode .content-inner a :after,body.night-mode .content-inner a :before,body.night-mode .content-inner a:after,body.night-mode .content-inner a:before{text-shadow:none}body.night-mode .content-inner a:visited{color:#D2D2D2}body.night-mode .content-inner .summary h2 a,body.night-mode .content-inner a.view-source{background:0 0;text-shadow:none}body.night-mode .content-inner .detail-header{background:#3A4152;color:#D2D2D2}body.night-mode .content-inner code,body.night-mode .content-inner pre code.hljs{background-color:#2C2C31}body.night-mode .content-inner pre a{text-shadow:none;background-image:none}body.night-mode .content-inner .footer{color:#959595}body.night-mode .content-inner .footer .line{display:inline-block}body.night-mode .content-inner .footer a{color:#959595;text-shadow:none;background-image:none;text-decoration:underline}.night-mode .sidebar-toggle i{color:#d5dae6}@media print{#sidebar{display:none}} --------------------------------------------------------------------------------