├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark.cr ├── benchmark.rb ├── docs ├── Crystagiri.html ├── Crystagiri │ ├── HTML.html │ └── Tag.html ├── css │ └── style.css ├── index.html ├── index.json ├── js │ └── doc.js └── search-index.js ├── shard.yml ├── spec ├── crystagiri_html_spec.cr ├── crystagiri_spec.cr ├── crystagiri_tag_spec.cr ├── fixture │ └── HTML.html └── spec_helper.cr └── src ├── crystagiri.cr └── crystagiri ├── html.cr ├── tag.cr └── version.cr /.gitignore: -------------------------------------------------------------------------------- 1 | /libs/ 2 | /lib/ 3 | /bin/ 4 | /.shards/ 5 | 6 | # Libraries don't need dependency lock 7 | # Dependencies will be locked in application that uses them 8 | /shard.lock 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: crystal 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 madeindjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crystagiri 2 | 3 | An HTML parser library for Crystal like the amazing [Nokogiri](https://github.com/sparklemotion/nokogiri) Ruby gem. 4 | 5 | > I won't pretend that **Crystagiri** does much as **Nokogiri**. All help is welcome! :) 6 | 7 | ## Installation 8 | 9 | Add this to your application's `shard.yml`: 10 | 11 | ```yaml 12 | dependencies: 13 | crystagiri: 14 | github: madeindjs/crystagiri 15 | ``` 16 | 17 | and then run 18 | 19 | ```bash 20 | $ shards install 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```crystal 26 | require "crystagiri" 27 | ``` 28 | 29 | Then you can simply instantiate a `Crystagiri::HTML` object from an HTML `String` like this 30 | 31 | ```crystal 32 | doc = Crystagiri::HTML.new "

Crystagiri is awesome!!

" 33 | ``` 34 | 35 | ... or directly load it from a Web URL or a pathname: 36 | 37 | ```crystal 38 | doc = Crystagiri::HTML.from_file "README.md" 39 | doc = Crystagiri::HTML.from_url "http://example.com/" 40 | ``` 41 | 42 | > Also you can specify `follow: true` flag if you want to follow redirect URL 43 | 44 | Then you can search all [`XML::Node`](https://crystal-lang.org/api/XML/Node.html)s from the `Crystagiri::HTML` instance. The tags found will be `Crystagiri::Tag` objects with the `.node` property: 45 | 46 | * CSS query 47 | 48 | ```Crystal 49 | puts doc.css("li > strong.title") { |tag| puts tag.node} 50 | # => .. 51 | # => .. 52 | ``` 53 | 54 | > **Known limitations**: Currently, you can't use CSS queries with complex search specifiers like `:nth-child` 55 | 56 | * HTML tag 57 | 58 | ```Crystal 59 | doc.where_tag("h2") { |tag| puts tag.content } 60 | # => Development 61 | # => Contributing 62 | ``` 63 | 64 | * HTML id 65 | 66 | ```Crystal 67 | puts doc.at_id("main-content").tagname 68 | # => div 69 | ``` 70 | 71 | * HTML class attribute 72 | 73 | ```Crystal 74 | doc.where_class("summary") { |tag| puts tag.node } 75 | # =>
..
76 | # =>
..
77 | # =>
..
78 | ``` 79 | 80 | ## Benchmark 81 | 82 | I know you love benchmarks between **Ruby** & **Crystal**, so here's one: 83 | 84 | ```ruby 85 | require "nokogiri" 86 | t1 = Time.now 87 | doc = Nokogiri::HTML File.read("spec/fixture/HTML.html") 88 | 1..100000.times do 89 | doc.at_css("h1") 90 | doc.css(".step-title"){ |tag| tag } 91 | end 92 | puts "executed in #{Time.now - t1} milliseconds" 93 | ``` 94 | 95 | > executed in 00:00:11.10 seconds with Ruby 2.6.0 with RVM on old Mac 96 | 97 | ```crystal 98 | require "crystagiri" 99 | t = Time.now 100 | doc = Crystagiri::HTML.from_file "./spec/fixture/HTML.html" 101 | 1..100000.times do 102 | doc.at_css("h1") 103 | doc.css(".step-title") { |tag| tag } 104 | end 105 | puts "executed in #{Time.now - t} milliseconds" 106 | ``` 107 | 108 | > executed in 00:00:03.09 seconds on Crystal 0.27.2 on LLVM 6.0.1 with release flag 109 | 110 | Crystagiri is more than **two time faster** than Nokogiri!! 111 | 112 | 113 | ## Development 114 | 115 | Clone this repository and navigate to it: 116 | 117 | ```bash 118 | $ git clone https://github.com/madeindjs/crystagiri.git 119 | $ cd crystagiri 120 | ``` 121 | 122 | You can generate all documentation with 123 | 124 | ```bash 125 | $ crystal doc 126 | ``` 127 | 128 | And run **spec** tests to ensure everything works correctly 129 | 130 | ```bash 131 | $ crystal spec 132 | ``` 133 | 134 | 135 | ## Contributing 136 | 137 | Do you like this project? [here](https://github.com/madeindjs/Crystagiri/issues/) you can find 138 | some issues to get started. 139 | 140 | Contributing is simple: 141 | 142 | 1. Fork it ( https://github.com/madeindjs/crystagiri/fork ) 143 | 2. Create your feature branch `git checkout -b my-new-feature` 144 | 3. Commit your changes `git commit -am "Add some feature"` 145 | 4. Push to the branch `git push origin my-new-feature` 146 | 5. Create a new Pull Request 147 | 148 | ## Contributors 149 | 150 | See the [list on Github](https://github.com/madeindjs/Crystagiri/graphs/contributors) 151 | -------------------------------------------------------------------------------- /benchmark.cr: -------------------------------------------------------------------------------- 1 | require "./src/crystagiri" 2 | t = Time.now 3 | doc = Crystagiri::HTML.from_file "./spec/fixture/HTML.html" 4 | 1..100000.times do 5 | doc.at_css("h1") 6 | doc.css(".step-title") { |tag| tag } 7 | end 8 | puts "executed in #{Time.now - t} seconds" 9 | -------------------------------------------------------------------------------- /benchmark.rb: -------------------------------------------------------------------------------- 1 | require "nokogiri" 2 | t1 = Time.now 3 | doc = Nokogiri::HTML File.read("spec/fixture/HTML.html") 4 | 1..100000.times do 5 | doc.at_css("h1") 6 | doc.css(".step-title"){|tag| tag} 7 | end 8 | puts "executed in #{Time.now - t1} seconds" -------------------------------------------------------------------------------- /docs/Crystagiri.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystagiri - github.com/madeindjs/Crystagiri 18 | 19 | 20 | 21 | 63 | 64 | 65 |
66 |

67 | 68 | module Crystagiri 69 | 70 |

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |

Defined in:

90 | 91 | 92 | 93 | crystagiri/html.cr 94 | 95 | 96 |
97 | 98 | 99 | 100 | crystagiri/tag.cr 101 | 102 | 103 |
104 | 105 | 106 | 107 | crystagiri/version.cr 108 | 109 | 110 |
111 | 112 | 113 | 114 | crystagiri.cr 115 | 116 | 117 |
118 | 119 | 120 | 121 | 122 | 123 |

Constant Summary

124 | 125 |
126 | 127 |
128 | VERSION = "0.4.0-alpha" 129 |
130 | 131 | 132 |
133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 |
145 | 146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 |
157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /docs/Crystagiri/HTML.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystagiri::HTML - github.com/madeindjs/Crystagiri 18 | 19 | 20 | 21 | 63 | 64 | 65 |
66 |

67 | 68 | class Crystagiri::HTML 69 | 70 |

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

Overview

79 | 80 |

Represent an Html document who can be parsed

81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |

Defined in:

96 | 97 | 98 | 99 | crystagiri/html.cr 100 | 101 | 102 |
103 | 104 | 105 | 106 | 107 | 108 | 109 |

Constructors

110 | 134 | 135 | 136 | 137 |

Class Method Summary

138 | 148 | 149 | 150 | 151 |

Instance Method Summary

152 | 207 | 208 | 209 | 210 | 211 | 212 |
213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 |
235 | 236 | 237 |

Constructor Detail

238 | 239 |
240 |
241 | 242 | def self.from_file(path : String) : HTML 243 | 244 | # 245 |
246 | 247 |

Initialize an Html object from content of file 248 | designed by the given filepath

249 | 250 |
251 |
252 | 253 | [View source] 254 | 255 |
256 |
257 | 258 |
259 |
260 | 261 | def self.from_url(url : String, follow : Bool = false) : HTML 262 | 263 | # 264 |
265 | 266 |

Initialize an Html object from Html source fetched 267 | from the url

268 | 269 |
270 |
271 | 272 | [View source] 273 | 274 |
275 |
276 | 277 |
278 |
279 | 280 | def self.new(content : String) 281 | 282 | # 283 |
284 | 285 |

Initialize an Html object from Html source

286 | 287 |
288 |
289 | 290 | [View source] 291 | 292 |
293 |
294 | 295 | 296 | 297 | 298 |

Class Method Detail

299 | 300 |
301 |
302 | 303 | def self.css_query_to_xpath(query : String) : String 304 | 305 | # 306 |
307 | 308 |

Transform the css query into an xpath query

309 | 310 |
311 |
312 | 313 | [View source] 314 | 315 |
316 |
317 | 318 | 319 | 320 | 321 |

Instance Method Detail

322 | 323 |
324 |
325 | 326 | def at_css(query : String) 327 | 328 | # 329 |
330 | 331 |

Find first node corresponding to the css query and return 332 | Crystagiri::Tag if founded or a nil if not founded

333 | 334 |
335 |
336 | 337 | [View source] 338 | 339 |
340 |
341 | 342 |
343 |
344 | 345 | def at_id(id_name : String) : Crystagiri::Tag? 346 | 347 | # 348 |
349 | 350 |

Find a node by its id and return a 351 | Crystagiri::Tag founded or a nil if not founded

352 | 353 |
354 |
355 | 356 | [View source] 357 | 358 |
359 |
360 | 361 |
362 |
363 | 364 | def at_tag(tag_name : String) : Crystagiri::Tag? 365 | 366 | # 367 |
368 | 369 |

Find first tag by tag name and return 370 | Crystagiri::Tag founded or a nil if not founded

371 | 372 |
373 |
374 | 375 | [View source] 376 | 377 |
378 |
379 | 380 |
381 |
382 | 383 | def content : String 384 | 385 | # 386 |
387 | 388 |
389 |
390 | 391 | [View source] 392 | 393 |
394 |
395 | 396 |
397 |
398 | 399 | def css(query : String, &block) : Array(Tag) 400 | 401 | # 402 |
403 | 404 |

Find all node corresponding to the css query and yield 405 | Crystagiri::Tag founded or a nil if not founded

406 | 407 |
408 |
409 | 410 | [View source] 411 | 412 |
413 |
414 | 415 |
416 |
417 | 418 | def nodes : XML::Node 419 | 420 | # 421 |
422 | 423 |
424 |
425 | 426 | [View source] 427 | 428 |
429 |
430 | 431 |
432 |
433 | 434 | def where_class(class_name : String, &block) : Array(Tag) 435 | 436 | # 437 |
438 | 439 |

Find all nodes by classname and yield 440 | Crystagiri::Tag founded

441 | 442 |
443 |
444 | 445 | [View source] 446 | 447 |
448 |
449 | 450 |
451 |
452 | 453 | def where_tag(tag_name : String, &block) : Array(Tag) 454 | 455 | # 456 |
457 | 458 |

Find all nodes by tag name and yield 459 | Crystagiri::Tag founded

460 | 461 |
462 |
463 | 464 | [View source] 465 | 466 |
467 |
468 | 469 | 470 | 471 | 472 | 473 |
474 | 475 | 476 | 477 | -------------------------------------------------------------------------------- /docs/Crystagiri/Tag.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | Crystagiri::Tag - github.com/madeindjs/Crystagiri 18 | 19 | 20 | 21 | 63 | 64 | 65 |
66 |

67 | 68 | class Crystagiri::Tag 69 | 70 |

71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 |

Defined in:

92 | 93 | 94 | 95 | crystagiri/tag.cr 96 | 97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 |

Constructors

106 | 114 | 115 | 116 | 117 | 118 | 119 |

Instance Method Summary

120 | 158 | 159 | 160 | 161 | 162 | 163 |
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
186 | 187 | 188 |

Constructor Detail

189 | 190 |
191 |
192 | 193 | def self.new(node : XML::Node) 194 | 195 | # 196 |
197 | 198 |
199 |
200 | 201 | [View source] 202 | 203 |
204 |
205 | 206 | 207 | 208 | 209 | 210 | 211 |

Instance Method Detail

212 | 213 |
214 |
215 | 216 | def children : Array(Tag) 217 | 218 | # 219 |
220 | 221 |
222 |
223 | 224 | [View source] 225 | 226 |
227 |
228 | 229 |
230 |
231 | 232 | def classname : String? 233 | 234 | # 235 |
236 | 237 |
238 |
239 | 240 | [View source] 241 | 242 |
243 |
244 | 245 |
246 |
247 | 248 | def content : String 249 | 250 | # 251 |
252 | 253 |
254 |
255 | 256 | [View source] 257 | 258 |
259 |
260 | 261 |
262 |
263 | 264 | def has_class?(klass : String) : Bool 265 | 266 | # 267 |
268 | 269 |
270 |
271 | 272 | [View source] 273 | 274 |
275 |
276 | 277 |
278 |
279 | 280 | def node : XML::Node 281 | 282 | # 283 |
284 | 285 |
286 |
287 | 288 | [View source] 289 | 290 |
291 |
292 | 293 |
294 |
295 | 296 | def parent : Tag? 297 | 298 | # 299 |
300 | 301 |
302 |
303 | 304 | [View source] 305 | 306 |
307 |
308 | 309 |
310 |
311 | 312 | def tagname : String 313 | 314 | # 315 |
316 | 317 |
318 |
319 | 320 | [View source] 321 | 322 |
323 |
324 | 325 | 326 | 327 | 328 | 329 |
330 | 331 | 332 | 333 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #FFFFFF; 3 | position: relative; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | font-family: "Avenir", "Tahoma", "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; 13 | color: #333; 14 | line-height: 1.5; 15 | } 16 | 17 | a { 18 | color: #263F6C; 19 | } 20 | 21 | a:visited { 22 | color: #112750; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 35px 0 25px; 27 | color: #444444; 28 | } 29 | 30 | h1.type-name { 31 | color: #47266E; 32 | margin: 20px 0 30px; 33 | background-color: #F8F8F8; 34 | padding: 10px 12px; 35 | border: 1px solid #EBEBEB; 36 | border-radius: 2px; 37 | } 38 | 39 | h2 { 40 | border-bottom: 1px solid #E6E6E6; 41 | padding-bottom: 5px; 42 | } 43 | 44 | body { 45 | display: flex; 46 | } 47 | 48 | .sidebar, .main-content { 49 | overflow: auto; 50 | } 51 | 52 | .sidebar { 53 | width: 30em; 54 | color: #F8F4FD; 55 | background-color: #2E1052; 56 | padding: 0 0 30px; 57 | box-shadow: inset -3px 0 4px rgba(0,0,0,.35); 58 | line-height: 1.2; 59 | } 60 | 61 | .sidebar .search-box { 62 | padding: 8px 9px; 63 | } 64 | 65 | .sidebar input { 66 | display: block; 67 | box-sizing: border-box; 68 | margin: 0; 69 | padding: 5px; 70 | font: inherit; 71 | font-family: inherit; 72 | line-height: 1.2; 73 | width: 100%; 74 | border: 0; 75 | outline: 0; 76 | border-radius: 2px; 77 | box-shadow: 0px 3px 5px rgba(0,0,0,.25); 78 | transition: box-shadow .12s; 79 | } 80 | 81 | .sidebar input:focus { 82 | box-shadow: 0px 5px 6px rgba(0,0,0,.5); 83 | } 84 | 85 | .sidebar input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 86 | color: #C8C8C8; 87 | font-size: 14px; 88 | text-indent: 2px; 89 | } 90 | 91 | .sidebar input::-moz-placeholder { /* Firefox 19+ */ 92 | color: #C8C8C8; 93 | font-size: 14px; 94 | text-indent: 2px; 95 | } 96 | 97 | .sidebar input:-ms-input-placeholder { /* IE 10+ */ 98 | color: #C8C8C8; 99 | font-size: 14px; 100 | text-indent: 2px; 101 | } 102 | 103 | .sidebar input:-moz-placeholder { /* Firefox 18- */ 104 | color: #C8C8C8; 105 | font-size: 14px; 106 | text-indent: 2px; 107 | } 108 | 109 | .sidebar ul { 110 | margin: 0; 111 | padding: 0; 112 | list-style: none outside; 113 | } 114 | 115 | .sidebar li { 116 | display: block; 117 | position: relative; 118 | } 119 | 120 | .types-list li.hide { 121 | display: none; 122 | } 123 | 124 | .sidebar a { 125 | text-decoration: none; 126 | color: inherit; 127 | transition: color .14s; 128 | } 129 | .types-list a { 130 | display: block; 131 | padding: 5px 15px 5px 30px; 132 | } 133 | 134 | .types-list { 135 | display: block; 136 | } 137 | 138 | .sidebar a:focus { 139 | outline: 1px solid #D1B7F1; 140 | } 141 | 142 | .types-list a { 143 | padding: 5px 15px 5px 30px; 144 | } 145 | 146 | .sidebar .current > a, 147 | .sidebar a:hover { 148 | color: #866BA6; 149 | } 150 | 151 | .repository-links { 152 | padding: 5px 15px 5px 30px; 153 | } 154 | 155 | .types-list li ul { 156 | overflow: hidden; 157 | height: 0; 158 | max-height: 0; 159 | transition: 1s ease-in-out; 160 | } 161 | 162 | .types-list li.parent { 163 | padding-left: 30px; 164 | } 165 | 166 | .types-list li.parent::before { 167 | box-sizing: border-box; 168 | content: "▼"; 169 | display: block; 170 | width: 30px; 171 | height: 30px; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | text-align: center; 176 | color: white; 177 | font-size: 8px; 178 | line-height: 30px; 179 | transform: rotateZ(-90deg); 180 | cursor: pointer; 181 | transition: .2s linear; 182 | } 183 | 184 | 185 | .types-list li.parent > a { 186 | padding-left: 0; 187 | } 188 | 189 | .types-list li.parent.open::before { 190 | transform: rotateZ(0); 191 | } 192 | 193 | .types-list li.open > ul { 194 | height: auto; 195 | max-height: 1000em; 196 | } 197 | 198 | .main-content { 199 | padding: 0 30px 30px 30px; 200 | width: 100%; 201 | } 202 | 203 | .kind { 204 | font-size: 60%; 205 | color: #866BA6; 206 | } 207 | 208 | .superclass-hierarchy { 209 | margin: -15px 0 30px 0; 210 | padding: 0; 211 | list-style: none outside; 212 | font-size: 80%; 213 | } 214 | 215 | .superclass-hierarchy .superclass { 216 | display: inline-block; 217 | margin: 0 7px 0 0; 218 | padding: 0; 219 | } 220 | 221 | .superclass-hierarchy .superclass + .superclass::before { 222 | content: "<"; 223 | margin-right: 7px; 224 | } 225 | 226 | .other-types-list li { 227 | display: inline-block; 228 | } 229 | 230 | .other-types-list, 231 | .list-summary { 232 | margin: 0 0 30px 0; 233 | padding: 0; 234 | list-style: none outside; 235 | } 236 | 237 | .entry-const { 238 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 239 | } 240 | 241 | .entry-const code { 242 | white-space: pre-wrap; 243 | } 244 | 245 | .entry-summary { 246 | padding-bottom: 4px; 247 | } 248 | 249 | .superclass-hierarchy .superclass a, 250 | .other-type a, 251 | .entry-summary .signature { 252 | padding: 4px 8px; 253 | margin-bottom: 4px; 254 | display: inline-block; 255 | background-color: #f8f8f8; 256 | color: #47266E; 257 | border: 1px solid #f0f0f0; 258 | text-decoration: none; 259 | border-radius: 3px; 260 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 261 | transition: background .15s, border-color .15s; 262 | } 263 | 264 | .superclass-hierarchy .superclass a:hover, 265 | .other-type a:hover, 266 | .entry-summary .signature:hover { 267 | background: #D5CAE3; 268 | border-color: #624288; 269 | } 270 | 271 | .entry-summary .summary { 272 | padding-left: 32px; 273 | } 274 | 275 | .entry-summary .summary p { 276 | margin: 12px 0 16px; 277 | } 278 | 279 | .entry-summary a { 280 | text-decoration: none; 281 | } 282 | 283 | .entry-detail { 284 | padding: 30px 0; 285 | } 286 | 287 | .entry-detail .signature { 288 | position: relative; 289 | padding: 5px 15px; 290 | margin-bottom: 10px; 291 | display: block; 292 | border-radius: 5px; 293 | background-color: #f8f8f8; 294 | color: #47266E; 295 | border: 1px solid #f0f0f0; 296 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 297 | transition: .2s ease-in-out; 298 | } 299 | 300 | .entry-detail:target .signature { 301 | background-color: #D5CAE3; 302 | border: 1px solid #624288; 303 | } 304 | 305 | .entry-detail .signature .method-permalink { 306 | position: absolute; 307 | top: 0; 308 | left: -35px; 309 | padding: 5px 15px; 310 | text-decoration: none; 311 | font-weight: bold; 312 | color: #624288; 313 | opacity: .4; 314 | transition: opacity .2s; 315 | } 316 | 317 | .entry-detail .signature .method-permalink:hover { 318 | opacity: 1; 319 | } 320 | 321 | .entry-detail:target .signature .method-permalink { 322 | opacity: 1; 323 | } 324 | 325 | .methods-inherited { 326 | padding-right: 10%; 327 | line-height: 1.5em; 328 | } 329 | 330 | .methods-inherited h3 { 331 | margin-bottom: 4px; 332 | } 333 | 334 | .methods-inherited a { 335 | display: inline-block; 336 | text-decoration: none; 337 | color: #47266E; 338 | } 339 | 340 | .methods-inherited a:hover { 341 | text-decoration: underline; 342 | color: #6C518B; 343 | } 344 | 345 | .methods-inherited .tooltip>span { 346 | background: #D5CAE3; 347 | padding: 4px 8px; 348 | border-radius: 3px; 349 | margin: -4px -8px; 350 | } 351 | 352 | .methods-inherited .tooltip * { 353 | color: #47266E; 354 | } 355 | 356 | pre { 357 | padding: 10px 20px; 358 | margin-top: 4px; 359 | border-radius: 3px; 360 | line-height: 1.45; 361 | overflow: auto; 362 | color: #333; 363 | background: #fdfdfd; 364 | font-size: 14px; 365 | border: 1px solid #eee; 366 | } 367 | 368 | code { 369 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 370 | } 371 | 372 | :not(pre) > code { 373 | background-color: rgba(40,35,30,0.05); 374 | padding: 0.2em 0.4em; 375 | font-size: 85%; 376 | border-radius: 3px; 377 | } 378 | 379 | span.flag { 380 | padding: 2px 4px 1px; 381 | border-radius: 3px; 382 | margin-right: 3px; 383 | font-size: 11px; 384 | border: 1px solid transparent; 385 | } 386 | 387 | span.flag.orange { 388 | background-color: #EE8737; 389 | color: #FCEBDD; 390 | border-color: #EB7317; 391 | } 392 | 393 | span.flag.yellow { 394 | background-color: #E4B91C; 395 | color: #FCF8E8; 396 | border-color: #B69115; 397 | } 398 | 399 | span.flag.green { 400 | background-color: #469C14; 401 | color: #E2F9D3; 402 | border-color: #34700E; 403 | } 404 | 405 | span.flag.red { 406 | background-color: #BF1919; 407 | color: #F9ECEC; 408 | border-color: #822C2C; 409 | } 410 | 411 | span.flag.purple { 412 | background-color: #2E1052; 413 | color: #ECE1F9; 414 | border-color: #1F0B37; 415 | } 416 | 417 | .tooltip>span { 418 | position: absolute; 419 | opacity: 0; 420 | display: none; 421 | pointer-events: none; 422 | } 423 | 424 | .tooltip:hover>span { 425 | display: inline-block; 426 | opacity: 1; 427 | } 428 | 429 | .c { 430 | color: #969896; 431 | } 432 | 433 | .n { 434 | color: #0086b3; 435 | } 436 | 437 | .t { 438 | color: #0086b3; 439 | } 440 | 441 | .s { 442 | color: #183691; 443 | } 444 | 445 | .i { 446 | color: #7f5030; 447 | } 448 | 449 | .k { 450 | color: #a71d5d; 451 | } 452 | 453 | .o { 454 | color: #a71d5d; 455 | } 456 | 457 | .m { 458 | color: #795da3; 459 | } 460 | 461 | .hidden { 462 | display: none; 463 | } 464 | .search-results { 465 | font-size: 90%; 466 | line-height: 1.3; 467 | } 468 | 469 | .search-results mark { 470 | color: inherit; 471 | background: transparent; 472 | font-weight: bold; 473 | } 474 | .search-result { 475 | padding: 5px 8px 5px 5px; 476 | cursor: pointer; 477 | border-left: 5px solid transparent; 478 | transform: translateX(-3px); 479 | transition: all .2s, background-color 0s, border .02s; 480 | min-height: 3.2em; 481 | } 482 | .search-result.current { 483 | border-left-color: #ddd; 484 | background-color: rgba(200,200,200,0.4); 485 | transform: translateX(0); 486 | transition: all .2s, background-color .5s, border 0s; 487 | } 488 | .search-result.current:hover, 489 | .search-result.current:focus { 490 | border-left-color: #866BA6; 491 | } 492 | .search-result:not(.current):nth-child(2n) { 493 | background-color: rgba(255,255,255,.06); 494 | } 495 | .search-result__title { 496 | font-size: 105%; 497 | word-break: break-all; 498 | line-height: 1.1; 499 | padding: 3px 0; 500 | } 501 | .search-result__title strong { 502 | font-weight: normal; 503 | } 504 | .search-results .search-result__title > a { 505 | padding: 0; 506 | display: block; 507 | } 508 | .search-result__title > a > .args { 509 | color: #dddddd; 510 | font-weight: 300; 511 | transition: inherit; 512 | font-size: 88%; 513 | line-height: 1.2; 514 | letter-spacing: -.02em; 515 | } 516 | .search-result__title > a > .args * { 517 | color: inherit; 518 | } 519 | 520 | .search-result a, 521 | .search-result a:hover { 522 | color: inherit; 523 | } 524 | .search-result:not(.current):hover .search-result__title > a, 525 | .search-result:not(.current):focus .search-result__title > a, 526 | .search-result__title > a:focus { 527 | color: #866BA6; 528 | } 529 | .search-result:not(.current):hover .args, 530 | .search-result:not(.current):focus .args { 531 | color: #6a5a7d; 532 | } 533 | 534 | .search-result__type { 535 | color: #e8e8e8; 536 | font-weight: 300; 537 | } 538 | .search-result__doc { 539 | color: #bbbbbb; 540 | font-size: 90%; 541 | } 542 | .search-result__doc p { 543 | margin: 0; 544 | text-overflow: ellipsis; 545 | display: -webkit-box; 546 | -webkit-box-orient: vertical; 547 | -webkit-line-clamp: 2; 548 | overflow: hidden; 549 | line-height: 1.2em; 550 | max-height: 2.4em; 551 | } 552 | 553 | .js-modal-visible .modal-background { 554 | display: flex; 555 | } 556 | .main-content { 557 | position: relative; 558 | } 559 | .modal-background { 560 | position: absolute; 561 | display: none; 562 | height: 100%; 563 | width: 100%; 564 | background: rgba(120,120,120,.4); 565 | z-index: 100; 566 | align-items: center; 567 | justify-content: center; 568 | } 569 | .usage-modal { 570 | max-width: 90%; 571 | background: #fff; 572 | border: 2px solid #ccc; 573 | border-radius: 9px; 574 | padding: 5px 15px 20px; 575 | min-width: 50%; 576 | color: #555; 577 | position: relative; 578 | transform: scale(.5); 579 | transition: transform 200ms; 580 | } 581 | .js-modal-visible .usage-modal { 582 | transform: scale(1); 583 | } 584 | .usage-modal > .close-button { 585 | position: absolute; 586 | right: 15px; 587 | top: 8px; 588 | color: #aaa; 589 | font-size: 27px; 590 | cursor: pointer; 591 | } 592 | .usage-modal > .close-button:hover { 593 | text-shadow: 2px 2px 2px #ccc; 594 | color: #999; 595 | } 596 | .modal-title { 597 | margin: 0; 598 | text-align: center; 599 | font-weight: normal; 600 | color: #666; 601 | border-bottom: 2px solid #ddd; 602 | padding: 10px; 603 | } 604 | .usage-list { 605 | padding: 0; 606 | margin: 13px; 607 | } 608 | .usage-list > li { 609 | padding: 5px 2px; 610 | overflow: auto; 611 | padding-left: 100px; 612 | min-width: 12em; 613 | } 614 | .usage-modal kbd { 615 | background: #eee; 616 | border: 1px solid #ccc; 617 | border-bottom-width: 2px; 618 | border-radius: 3px; 619 | padding: 3px 8px; 620 | font-family: monospace; 621 | margin-right: 2px; 622 | display: inline-block; 623 | } 624 | .usage-key { 625 | float: left; 626 | clear: left; 627 | margin-left: -100px; 628 | margin-right: 12px; 629 | } 630 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | README - github.com/madeindjs/Crystagiri 18 | 19 | 20 | 21 | 63 | 64 | 65 |
66 |

Crystagiri

67 | 68 |

An HTML parser library for Crystal like the amazing Nokogiri Ruby gem.

69 | 70 |
I won't pretend that Crystagiri does much as Nokogiri. All help is welcome! :)
71 | 72 |

Installation

73 | 74 |

Add this to your application's shard.yml:

75 | 76 |
dependencies:
 77 |   crystagiri:
 78 |     github: madeindjs/crystagiri
79 | 80 |

and then run

81 | 82 |
$ crystal deps
83 | 84 |

Usage

85 | 86 |
require "crystagiri"
87 | 88 |

Then you can simply instantiate a Crystagiri::HTML object from an HTML String like this

89 | 90 |
doc = Crystagiri::HTML.new "<h1>Crystagiri is awesome!!</h1>"
91 | 92 |

... or directly load it from a Web URL or a pathname:

93 | 94 |
doc = Crystagiri::HTML.from_file "README.md"
 95 | doc = Crystagiri::HTML.from_url "http://example.com/"
96 | 97 |
Also you can specify follow: true flag if you want to follow redirect URL
98 | 99 |

Then you can search all XML::Nodes from the Crystagiri::HTML instance. The tags found will be Crystagiri::Tag objects with the .node property:

100 | 101 | 102 | 103 |
puts doc.css("li > strong.title") { |tag| puts tag.node}
104 | # => <strong class="title"> .. </strong>
105 | # => <strong class="title"> .. </strong>
106 | 107 |
Known limitations: Currently, you can't use CSS queries with complex search specifiers like :nth-child
108 | 109 | 110 | 111 |
doc.where_tag("h2") { |tag| puts tag.content }
112 | # => Development
113 | # => Contributing
114 | 115 | 116 | 117 |
puts doc.at_id("main-content").tagname
118 | # => div
119 | 120 | 121 | 122 |
doc.where_class("summary") { |tag| puts tag.node }
123 | # => <div class="summary"> .. </div>
124 | # => <div class="summary"> .. </div>
125 | # => <div class="summary"> .. </div>
126 | 127 |

Benchmark

128 | 129 |

I know you love benchmarks between Ruby & Crystal, so here's one:

130 | 131 |
require "nokogiri"
132 | t1 = Time.now
133 | doc = Nokogiri::HTML File.read("spec/fixture/HTML.html")
134 | 1..100000.times do
135 |   doc.at_css("h1")
136 |   doc.css(".step-title"){ |tag| tag }
137 | end
138 | puts "executed in #{Time.now - t1} milliseconds"
139 | 140 |
executed in 00:00:11.10 seconds with Ruby 2.6.0 with RVM on old Mac
141 | 142 |
require "crystagiri"
143 | t = Time.now
144 | doc = Crystagiri::HTML.from_file "./spec/fixture/HTML.html"
145 | 1..100000.times do
146 |   doc.at_css("h1")
147 |   doc.css(".step-title") { |tag| tag }
148 | end
149 | puts "executed in #{Time.now - t} milliseconds"
150 | 151 |
executed in 00:00:03.09 seconds on Crystal 0.27.2 on LLVM 6.0.1 with release flag
152 | 153 |

Crystagiri is more than two time faster than Nokogiri!!

154 | 155 |

Development

156 | 157 |

Clone this repository and navigate to it:

158 | 159 |
$ git clone https://github.com/madeindjs/crystagiri.git
160 | $ cd crystagiri
161 | 162 |

You can generate all documentation with

163 | 164 |
$ crystal doc
165 | 166 |

And run spec tests to ensure everything works correctly

167 | 168 |
$ crystal spec
169 | 170 |

Contributing

171 | 172 |
  1. Fork it ( https://github.com/madeindjs/crystagiri/fork )
  2. Create your feature branch git checkout -b my-new-feature
  3. Commit your changes git commit -am "Add some feature"
  4. Push to the branch git push origin my-new-feature
  5. Create a new Pull Request
173 | 174 |

Contributors

175 | 176 |

See the list on Github

177 |
178 | 179 | 180 | -------------------------------------------------------------------------------- /docs/index.json: -------------------------------------------------------------------------------- 1 | {"repository_name":"github.com/madeindjs/Crystagiri","body":"# Crystagiri\n\nAn HTML parser library for Crystal like the amazing [Nokogiri](https://github.com/sparklemotion/nokogiri) Ruby gem.\n\n> I won't pretend that **Crystagiri** does much as **Nokogiri**. All help is welcome! :)\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n crystagiri:\n github: madeindjs/crystagiri\n```\n\nand then run\n\n```bash\n$ crystal deps\n```\n\n## Usage\n\n```crystal\nrequire \"crystagiri\"\n```\n\nThen you can simply instantiate a `Crystagiri::HTML` object from an HTML `String` like this\n\n```crystal\ndoc = Crystagiri::HTML.new \"

Crystagiri is awesome!!

\"\n```\n\n... or directly load it from a Web URL or a pathname:\n\n```crystal\ndoc = Crystagiri::HTML.from_file \"README.md\"\ndoc = Crystagiri::HTML.from_url \"http://example.com/\"\n```\n\n> Also you can specify `follow: true` flag if you want to follow redirect URL\n\nThen you can search all [`XML::Node`](https://crystal-lang.org/api/XML/Node.html)s from the `Crystagiri::HTML` instance. The tags found will be `Crystagiri::Tag` objects with the `.node` property:\n\n* CSS query\n\n```Crystal\nputs doc.css(\"li > strong.title\") { |tag| puts tag.node}\n# => .. \n# => .. \n```\n\n> **Known limitations**: Currently, you can't use CSS queries with complex search specifiers like `:nth-child`\n\n* HTML tag\n\n```Crystal\ndoc.where_tag(\"h2\") { |tag| puts tag.content }\n# => Development\n# => Contributing\n```\n\n* HTML id\n\n```Crystal\nputs doc.at_id(\"main-content\").tagname\n# => div\n```\n\n* HTML class attribute\n\n```Crystal\ndoc.where_class(\"summary\") { |tag| puts tag.node }\n# =>
..
\n# =>
..
\n# =>
..
\n```\n\n## Benchmark\n\nI know you love benchmarks between **Ruby** & **Crystal**, so here's one:\n\n```ruby\nrequire \"nokogiri\"\nt1 = Time.now\ndoc = Nokogiri::HTML File.read(\"spec/fixture/HTML.html\")\n1..100000.times do\n doc.at_css(\"h1\")\n doc.css(\".step-title\"){ |tag| tag }\nend\nputs \"executed in #{Time.now - t1} milliseconds\"\n```\n\n> executed in 00:00:11.10 seconds with Ruby 2.6.0 with RVM on old Mac\n\n```crystal\nrequire \"crystagiri\"\nt = Time.now\ndoc = Crystagiri::HTML.from_file \"./spec/fixture/HTML.html\"\n1..100000.times do\n doc.at_css(\"h1\")\n doc.css(\".step-title\") { |tag| tag }\nend\nputs \"executed in #{Time.now - t} milliseconds\"\n```\n\n> executed in 00:00:03.09 seconds on Crystal 0.27.2 on LLVM 6.0.1 with release flag\n\nCrystagiri is more than **two time faster** than Nokogiri!!\n\n\n## Development\n\nClone this repository and navigate to it:\n\n```bash\n$ git clone https://github.com/madeindjs/crystagiri.git\n$ cd crystagiri\n```\n\nYou can generate all documentation with\n\n```bash\n$ crystal doc\n```\n\nAnd run **spec** tests to ensure everything works correctly\n\n```bash\n$ crystal spec\n```\n\n\n## Contributing\n\n1. Fork it ( https://github.com/madeindjs/crystagiri/fork )\n2. Create your feature branch `git checkout -b my-new-feature`\n3. Commit your changes `git commit -am \"Add some feature\"`\n4. Push to the branch `git push origin my-new-feature`\n5. Create a new Pull Request\n\n## Contributors\n\nSee the [list on Github](https://github.com/madeindjs/Crystagiri/graphs/contributors)\n","program":{"html_id":"github.com/madeindjs/Crystagiri/toplevel","path":"toplevel.html","kind":"module","full_name":"Top Level Namespace","name":"Top Level Namespace","abstract":false,"superclass":null,"ancestors":[],"locations":[],"repository_name":"github.com/madeindjs/Crystagiri","program":true,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":null,"doc":null,"summary":null,"class_methods":[],"constructors":[],"instance_methods":[],"macros":[],"types":[{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","path":"Crystagiri.html","kind":"module","full_name":"Crystagiri","name":"Crystagiri","abstract":false,"superclass":null,"ancestors":[],"locations":[{"filename":"crystagiri/html.cr","line_number":4,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr"},{"filename":"crystagiri/tag.cr","line_number":1,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr"},{"filename":"crystagiri/version.cr","line_number":1,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/version.cr"},{"filename":"crystagiri.cr","line_number":3,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[{"id":"VERSION","name":"VERSION","value":"\"0.4.0-alpha\"","doc":null,"summary":null}],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":null,"doc":null,"summary":null,"class_methods":[],"constructors":[],"instance_methods":[],"macros":[],"types":[{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri/HTML","path":"Crystagiri/HTML.html","kind":"class","full_name":"Crystagiri::HTML","name":"HTML","abstract":false,"superclass":{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},"ancestors":[{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},{"html_id":"github.com/madeindjs/Crystagiri/Object","kind":"class","full_name":"Object","name":"Object"}],"locations":[{"filename":"crystagiri/html.cr","line_number":6,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","kind":"module","full_name":"Crystagiri","name":"Crystagiri"},"doc":"Represent an Html document who can be parsed","summary":"

Represent an Html document who can be parsed

","class_methods":[{"id":"css_query_to_xpath(query:String):String-class-method","html_id":"css_query_to_xpath(query:String):String-class-method","name":"css_query_to_xpath","doc":"Transform the css query into an xpath query","summary":"

Transform the css query into an xpath query

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String) : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L34","def":{"name":"css_query_to_xpath","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"query = \"//#{query}\"\nquery = query.gsub(/\\#([A-z0-9]+-*_*)+/) do |m|\n \"*[@id=\\\"%s\\\"]\" % (m.delete('#'))\nend\nquery = query.gsub(/\\.([A-z0-9]+-*_*)+/) do |m|\n \"[@class=\\\"%s\\\"]\" % (m.delete('.'))\nend\nquery = query.gsub(/\\s*>\\s*/) do |m|\n \"/\"\nend\nquery = query.gsub(\" \", \"//\")\nquery = query.gsub(/\\/\\[/) do |m|\n \"/*[\"\nend\nreturn query\n"}}],"constructors":[{"id":"from_file(path:String):HTML-class-method","html_id":"from_file(path:String):HTML-class-method","name":"from_file","doc":"Initialize an Html object from content of file\ndesigned by the given filepath","summary":"

Initialize an Html object from content of file designed by the given filepath

","abstract":false,"args":[{"name":"path","doc":null,"default_value":"","external_name":"path","restriction":"String"}],"args_string":"(path : String) : HTML","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L29","def":{"name":"from_file","args":[{"name":"path","doc":null,"default_value":"","external_name":"path","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"HTML","visibility":"Public","body":"return HTML.new(File.read(path))"}},{"id":"from_url(url:String,follow:Bool=false):HTML-class-method","html_id":"from_url(url:String,follow:Bool=false):HTML-class-method","name":"from_url","doc":"Initialize an Html object from Html source fetched\nfrom the url","summary":"

Initialize an Html object from Html source fetched from the url

","abstract":false,"args":[{"name":"url","doc":null,"default_value":"","external_name":"url","restriction":"String"},{"name":"follow","doc":null,"default_value":"false","external_name":"follow","restriction":"Bool"}],"args_string":"(url : String, follow : Bool = false) : HTML","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L12","def":{"name":"from_url","args":[{"name":"url","doc":null,"default_value":"","external_name":"url","restriction":"String"},{"name":"follow","doc":null,"default_value":"false","external_name":"follow","restriction":"Bool"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"HTML","visibility":"Public","body":"begin\n response = HTTP::Client.get(url)\n if response.status_code == 200\n return HTML.new(response.body)\n else\n if follow && (response.status_code == 301)\n from_url(response.headers[\"Location\"], follow: true)\n else\n raise(ArgumentError.new(\"Host returned #{response.status_code}\"))\n end\n end\nrescue Socket::Error\n raise(Socket::Error.new(\"Host #{url} cannot be fetched\"))\nend"}},{"id":"new(content:String)-class-method","html_id":"new(content:String)-class-method","name":"new","doc":"Initialize an Html object from Html source","summary":"

Initialize an Html object from Html source

","abstract":false,"args":[{"name":"content","doc":null,"default_value":"","external_name":"content","restriction":"String"}],"args_string":"(content : String)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L50","def":{"name":"new","args":[{"name":"content","doc":null,"default_value":"","external_name":"content","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"_ = allocate\n_.initialize(content)\nif _.responds_to?(:finalize)\n ::GC.add_finalizer(_)\nend\n_\n"}}],"instance_methods":[{"id":"at_css(query:String)-instance-method","html_id":"at_css(query:String)-instance-method","name":"at_css","doc":"Find first node corresponding to the css query and return\n`Crystagiri::Tag` if founded or a nil if not founded","summary":"

Find first node corresponding to the css query and return Crystagiri::Tag if founded or a nil if not founded

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L159","def":{"name":"at_css","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"css(query) do |tag|\n return tag\nend\nreturn nil\n"}},{"id":"at_id(id_name:String):Crystagiri::Tag?-instance-method","html_id":"at_id(id_name:String):Crystagiri::Tag?-instance-method","name":"at_id","doc":"Find a node by its id and return a\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find a node by its id and return a Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"id_name","doc":null,"default_value":"","external_name":"id_name","restriction":"String"}],"args_string":"(id_name : String) : Crystagiri::Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L140","def":{"name":"at_id","args":[{"name":"id_name","doc":null,"default_value":"","external_name":"id_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Crystagiri::Tag | Nil","visibility":"Public","body":"if node = @ids[id_name]?\n return (Tag.new(node)).as(Crystagiri::Tag)\nend"}},{"id":"at_tag(tag_name:String):Crystagiri::Tag?-instance-method","html_id":"at_tag(tag_name:String):Crystagiri::Tag?-instance-method","name":"at_tag","doc":"Find first tag by tag name and return\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find first tag by tag name and return Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"args_string":"(tag_name : String) : Crystagiri::Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L101","def":{"name":"at_tag","args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Crystagiri::Tag | Nil","visibility":"Public","body":"if tags = @tags[tag_name]?\n tags.each do |tag|\n return (Tag.new(tag)).as(Crystagiri::Tag)\n end\nend\nreturn nil\n"}},{"id":"content:String-instance-method","html_id":"content:String-instance-method","name":"content","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L7","def":{"name":"content","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@content"}},{"id":"css(query:String,&block):Array(Tag)-instance-method","html_id":"css(query:String,&block):Array(Tag)-instance-method","name":"css","doc":"Find all node corresponding to the css query and yield\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find all node corresponding to the css query and yield Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L148","def":{"name":"css","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"query = HTML.css_query_to_xpath(query)\nreturn (@nodes.xpath_nodes(\"//#{query}\")).map do |node|\n tag = (Tag.new(node)).as(Crystagiri::Tag)\n yield tag\n tag\nend\n"}},{"id":"nodes:XML::Node-instance-method","html_id":"nodes:XML::Node-instance-method","name":"nodes","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : XML::Node","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L8","def":{"name":"nodes","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@nodes"}},{"id":"where_class(class_name:String,&block):Array(Tag)-instance-method","html_id":"where_class(class_name:String,&block):Array(Tag)-instance-method","name":"where_class","doc":"Find all nodes by classname and yield\n`Crystagiri::Tag` founded","summary":"

Find all nodes by classname and yield Crystagiri::Tag founded

","abstract":false,"args":[{"name":"class_name","doc":null,"default_value":"","external_name":"class_name","restriction":"String"}],"args_string":"(class_name : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L126","def":{"name":"where_class","args":[{"name":"class_name","doc":null,"default_value":"","external_name":"class_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"arr = [] of Crystagiri::Tag\nif klasses = @classes[class_name]?\n klasses.each do |node|\n klass = (Tag.new(node)).as(Crystagiri::Tag)\n yield klass\n arr << klass\n end\nend\nreturn arr\n"}},{"id":"where_tag(tag_name:String,&block):Array(Tag)-instance-method","html_id":"where_tag(tag_name:String,&block):Array(Tag)-instance-method","name":"where_tag","doc":"Find all nodes by tag name and yield\n`Crystagiri::Tag` founded","summary":"

Find all nodes by tag name and yield Crystagiri::Tag founded

","abstract":false,"args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"args_string":"(tag_name : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L112","def":{"name":"where_tag","args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"arr = [] of Crystagiri::Tag\nif tags = @tags[tag_name]?\n tags.each do |node|\n tag = (Tag.new(node)).as(Crystagiri::Tag)\n yield tag\n arr << tag\n end\nend\nreturn arr\n"}}],"macros":[],"types":[]},{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri/Tag","path":"Crystagiri/Tag.html","kind":"class","full_name":"Crystagiri::Tag","name":"Tag","abstract":false,"superclass":{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},"ancestors":[{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},{"html_id":"github.com/madeindjs/Crystagiri/Object","kind":"class","full_name":"Object","name":"Object"}],"locations":[{"filename":"crystagiri/tag.cr","line_number":2,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","kind":"module","full_name":"Crystagiri","name":"Crystagiri"},"doc":null,"summary":null,"class_methods":[],"constructors":[{"id":"new(node:XML::Node)-class-method","html_id":"new(node:XML::Node)-class-method","name":"new","doc":null,"summary":null,"abstract":false,"args":[{"name":"node","doc":null,"default_value":"","external_name":"node","restriction":"XML::Node"}],"args_string":"(node : XML::Node)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L5","def":{"name":"new","args":[{"name":"node","doc":null,"default_value":"","external_name":"node","restriction":"XML::Node"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"_ = allocate\n_.initialize(node)\nif _.responds_to?(:finalize)\n ::GC.add_finalizer(_)\nend\n_\n"}}],"instance_methods":[{"id":"children:Array(Tag)-instance-method","html_id":"children:Array(Tag)-instance-method","name":"children","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L27","def":{"name":"children","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"children = [] of Tag\n@node.children.each do |node|\n if node.element?\n children << (Tag.new(node))\n end\nend\nchildren\n"}},{"id":"classname:String?-instance-method","html_id":"classname:String?-instance-method","name":"classname","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L8","def":{"name":"classname","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String | Nil","visibility":"Public","body":"return @node[\"class\"]? ? @node[\"class\"] : nil"}},{"id":"content:String-instance-method","html_id":"content:String-instance-method","name":"content","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L16","def":{"name":"content","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"return @node.text != nil ? @node.text.as(String) : \"\".as(String)"}},{"id":"has_class?(klass:String):Bool-instance-method","html_id":"has_class?(klass:String):Bool-instance-method","name":"has_class?","doc":null,"summary":null,"abstract":false,"args":[{"name":"klass","doc":null,"default_value":"","external_name":"klass","restriction":"String"}],"args_string":"(klass : String) : Bool","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L37","def":{"name":"has_class?","args":[{"name":"klass","doc":null,"default_value":"","external_name":"klass","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Bool","visibility":"Public","body":"if classes = classname\n return classes.includes?(klass)\nend\nfalse\n"}},{"id":"node:XML::Node-instance-method","html_id":"node:XML::Node-instance-method","name":"node","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : XML::Node","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L3","def":{"name":"node","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@node"}},{"id":"parent:Tag?-instance-method","html_id":"parent:Tag?-instance-method","name":"parent","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L20","def":{"name":"parent","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Tag | Nil","visibility":"Public","body":"if parent = @node.parent\n return Tag.new(parent)\nend\nnil\n"}},{"id":"tagname:String-instance-method","html_id":"tagname:String-instance-method","name":"tagname","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L12","def":{"name":"tagname","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"return @node.name"}}],"macros":[],"types":[]}]}]}} -------------------------------------------------------------------------------- /docs/js/doc.js: -------------------------------------------------------------------------------- 1 | window.CrystalDoc = (window.CrystalDoc || {}); 2 | 3 | CrystalDoc.base_path = (CrystalDoc.base_path || ""); 4 | 5 | CrystalDoc.searchIndex = (CrystalDoc.searchIndex || false); 6 | CrystalDoc.MAX_RESULTS_DISPLAY = 140; 7 | 8 | CrystalDoc.runQuery = function(query) { 9 | function searchType(type, query, results) { 10 | var matches = []; 11 | var matchedFields = []; 12 | var name = type.full_name; 13 | var i = name.lastIndexOf("::"); 14 | if (i > 0) { 15 | name = name.substring(i + 2); 16 | } 17 | var nameMatches = query.matches(name); 18 | if (nameMatches){ 19 | matches = matches.concat(nameMatches); 20 | matchedFields.push("name"); 21 | } 22 | 23 | var namespaceMatches = query.matchesNamespace(type.full_name); 24 | if(namespaceMatches){ 25 | matches = matches.concat(namespaceMatches); 26 | matchedFields.push("name"); 27 | } 28 | 29 | var docMatches = query.matches(type.doc); 30 | if(docMatches){ 31 | matches = matches.concat(docMatches); 32 | matchedFields.push("doc"); 33 | } 34 | if (matches.length > 0) { 35 | results.push({ 36 | id: type.id, 37 | result_type: "type", 38 | kind: type.kind, 39 | name: name, 40 | full_name: type.full_name, 41 | href: type.path, 42 | summary: type.summary, 43 | matched_fields: matchedFields, 44 | matched_terms: matches 45 | }); 46 | } 47 | 48 | type.instance_methods.forEach(function(method) { 49 | searchMethod(method, type, "instance_method", query, results); 50 | }) 51 | type.class_methods.forEach(function(method) { 52 | searchMethod(method, type, "class_method", query, results); 53 | }) 54 | type.constructors.forEach(function(constructor) { 55 | searchMethod(constructor, type, "constructor", query, results); 56 | }) 57 | type.macros.forEach(function(macro) { 58 | searchMethod(macro, type, "macro", query, results); 59 | }) 60 | type.constants.forEach(function(constant){ 61 | searchConstant(constant, type, query, results); 62 | }); 63 | 64 | type.types.forEach(function(subtype){ 65 | searchType(subtype, query, results); 66 | }); 67 | }; 68 | 69 | function searchMethod(method, type, kind, query, results) { 70 | var matches = []; 71 | var matchedFields = []; 72 | var nameMatches = query.matchesMethod(method.name, kind, type); 73 | if (nameMatches){ 74 | matches = matches.concat(nameMatches); 75 | matchedFields.push("name"); 76 | } 77 | 78 | method.args.forEach(function(arg){ 79 | var argMatches = query.matches(arg.external_name); 80 | if (argMatches) { 81 | matches = matches.concat(argMatches); 82 | matchedFields.push("args"); 83 | } 84 | }); 85 | 86 | var docMatches = query.matches(type.doc); 87 | if(docMatches){ 88 | matches = matches.concat(docMatches); 89 | matchedFields.push("doc"); 90 | } 91 | 92 | if (matches.length > 0) { 93 | var typeMatches = query.matches(type.full_name); 94 | if (typeMatches) { 95 | matchedFields.push("type"); 96 | matches = matches.concat(typeMatches); 97 | } 98 | results.push({ 99 | id: method.id, 100 | type: type.full_name, 101 | result_type: kind, 102 | name: method.name, 103 | full_name: type.full_name + "#" + method.name, 104 | args_string: method.args_string, 105 | summary: method.summary, 106 | href: type.path + "#" + method.id, 107 | matched_fields: matchedFields, 108 | matched_terms: matches 109 | }); 110 | } 111 | } 112 | 113 | function searchConstant(constant, type, query, results) { 114 | var matches = []; 115 | var matchedFields = []; 116 | var nameMatches = query.matches(constant.name); 117 | if (nameMatches){ 118 | matches = matches.concat(nameMatches); 119 | matchedFields.push("name"); 120 | } 121 | var docMatches = query.matches(constant.doc); 122 | if(docMatches){ 123 | matches = matches.concat(docMatches); 124 | matchedFields.push("doc"); 125 | } 126 | if (matches.length > 0) { 127 | var typeMatches = query.matches(type.full_name); 128 | if (typeMatches) { 129 | matchedFields.push("type"); 130 | matches = matches.concat(typeMatches); 131 | } 132 | results.push({ 133 | id: constant.id, 134 | type: type.full_name, 135 | result_type: "constant", 136 | name: constant.name, 137 | full_name: type.full_name + "#" + constant.name, 138 | value: constant.value, 139 | summary: constant.summary, 140 | href: type.path + "#" + constant.id, 141 | matched_fields: matchedFields, 142 | matched_terms: matches 143 | }); 144 | } 145 | } 146 | 147 | var results = []; 148 | searchType(CrystalDoc.searchIndex.program, query, results); 149 | return results; 150 | }; 151 | 152 | CrystalDoc.rankResults = function(results, query) { 153 | function uniqueArray(ar) { 154 | var j = {}; 155 | 156 | ar.forEach(function(v) { 157 | j[v + "::" + typeof v] = v; 158 | }); 159 | 160 | return Object.keys(j).map(function(v) { 161 | return j[v]; 162 | }); 163 | } 164 | 165 | results = results.sort(function(a, b) { 166 | var matchedTermsDiff = uniqueArray(b.matched_terms).length - uniqueArray(a.matched_terms).length; 167 | var aHasDocs = b.matched_fields.includes("doc"); 168 | var bHasDocs = b.matched_fields.includes("doc"); 169 | 170 | var aOnlyDocs = aHasDocs && a.matched_fields.length == 1; 171 | var bOnlyDocs = bHasDocs && b.matched_fields.length == 1; 172 | 173 | if (a.result_type == "type" && b.result_type != "type" && !aOnlyDocs) { 174 | if(CrystalDoc.DEBUG) { console.log("a is type b not"); } 175 | return -1; 176 | } else if (b.result_type == "type" && a.result_type != "type" && !bOnlyDocs) { 177 | if(CrystalDoc.DEBUG) { console.log("b is type, a not"); } 178 | return 1; 179 | } 180 | if (a.matched_fields.includes("name")) { 181 | if (b.matched_fields.includes("name")) { 182 | var a_name = (CrystalDoc.prefixForType(a.result_type) || "") + ((a.result_type == "type") ? a.full_name : a.name); 183 | var b_name = (CrystalDoc.prefixForType(b.result_type) || "") + ((b.result_type == "type") ? b.full_name : b.name); 184 | a_name = a_name.toLowerCase(); 185 | b_name = b_name.toLowerCase(); 186 | for(var i = 0; i < query.normalizedTerms.length; i++) { 187 | var term = query.terms[i].replace(/^::?|::?$/, ""); 188 | var a_orig_index = a_name.indexOf(term); 189 | var b_orig_index = b_name.indexOf(term); 190 | if(CrystalDoc.DEBUG) { console.log("term: " + term + " a: " + a_name + " b: " + b_name); } 191 | if(CrystalDoc.DEBUG) { console.log(a_orig_index, b_orig_index, a_orig_index - b_orig_index); } 192 | if (a_orig_index >= 0) { 193 | if (b_orig_index >= 0) { 194 | if(CrystalDoc.DEBUG) { console.log("both have exact match", a_orig_index > b_orig_index ? -1 : 1); } 195 | if(a_orig_index != b_orig_index) { 196 | if(CrystalDoc.DEBUG) { console.log("both have exact match at different positions", a_orig_index > b_orig_index ? 1 : -1); } 197 | return a_orig_index > b_orig_index ? 1 : -1; 198 | } 199 | } else { 200 | if(CrystalDoc.DEBUG) { console.log("a has exact match, b not"); } 201 | return -1; 202 | } 203 | } else if (b_orig_index >= 0) { 204 | if(CrystalDoc.DEBUG) { console.log("b has exact match, a not"); } 205 | return 1; 206 | } 207 | } 208 | } else { 209 | if(CrystalDoc.DEBUG) { console.log("a has match in name, b not"); } 210 | return -1; 211 | } 212 | } else if ( 213 | !a.matched_fields.includes("name") && 214 | b.matched_fields.includes("name") 215 | ) { 216 | return 1; 217 | } 218 | 219 | if (matchedTermsDiff != 0 || (aHasDocs != bHasDocs)) { 220 | if(CrystalDoc.DEBUG) { console.log("matchedTermsDiff: " + matchedTermsDiff, aHasDocs, bHasDocs); } 221 | return matchedTermsDiff; 222 | } 223 | 224 | var matchedFieldsDiff = b.matched_fields.length - a.matched_fields.length; 225 | if (matchedFieldsDiff != 0) { 226 | if(CrystalDoc.DEBUG) { console.log("matched to different number of fields: " + matchedFieldsDiff); } 227 | return matchedFieldsDiff > 0 ? 1 : -1; 228 | } 229 | 230 | var nameCompare = a.name.localeCompare(b.name); 231 | if(nameCompare != 0){ 232 | if(CrystalDoc.DEBUG) { console.log("nameCompare resulted in: " + a.name + "<=>" + b.name + ": " + nameCompare); } 233 | return nameCompare > 0 ? 1 : -1; 234 | } 235 | 236 | if(a.matched_fields.includes("args") && b.matched_fields.includes("args")) { 237 | for(var i = 0; i < query.terms.length; i++) { 238 | var term = query.terms[i]; 239 | var aIndex = a.args_string.indexOf(term); 240 | var bIndex = b.args_string.indexOf(term); 241 | if(CrystalDoc.DEBUG) { console.log("index of " + term + " in args_string: " + aIndex + " - " + bIndex); } 242 | if(aIndex >= 0){ 243 | if(bIndex >= 0){ 244 | if(aIndex != bIndex){ 245 | return aIndex > bIndex ? 1 : -1; 246 | } 247 | }else{ 248 | return -1; 249 | } 250 | }else if(bIndex >= 0) { 251 | return 1; 252 | } 253 | } 254 | } 255 | 256 | return 0; 257 | }); 258 | 259 | if (results.length > 1) { 260 | // if we have more than two search terms, only include results whith the most matches 261 | var bestMatchedTerms = uniqueArray(results[0].matched_terms).length; 262 | 263 | results = results.filter(function(result) { 264 | return uniqueArray(result.matched_terms).length + 1 >= bestMatchedTerms; 265 | }); 266 | } 267 | return results; 268 | }; 269 | 270 | CrystalDoc.prefixForType = function(type) { 271 | switch (type) { 272 | case "instance_method": 273 | return "#"; 274 | 275 | case "class_method": 276 | case "macro": 277 | case "constructor": 278 | return "."; 279 | 280 | default: 281 | return false; 282 | } 283 | }; 284 | 285 | CrystalDoc.displaySearchResults = function(results, query) { 286 | function sanitize(html){ 287 | return html.replace(/<(?!\/?code)[^>]+>/g, ""); 288 | } 289 | 290 | // limit results 291 | if (results.length > CrystalDoc.MAX_RESULTS_DISPLAY) { 292 | results = results.slice(0, CrystalDoc.MAX_RESULTS_DISPLAY); 293 | } 294 | 295 | var $frag = document.createDocumentFragment(); 296 | var $resultsElem = document.querySelector(".search-list"); 297 | $resultsElem.innerHTML = ""; 298 | 299 | results.forEach(function(result, i) { 300 | var url = CrystalDoc.base_path + result.href; 301 | var type = false; 302 | 303 | var title = query.highlight(result.result_type == "type" ? result.full_name : result.name); 304 | 305 | var prefix = CrystalDoc.prefixForType(result.result_type); 306 | if (prefix) { 307 | title = "" + prefix + "" + title; 308 | } 309 | 310 | title = "" + title + ""; 311 | 312 | if (result.args_string) { 313 | title += 314 | "" + query.highlight(result.args_string) + ""; 315 | } 316 | 317 | $elem = document.createElement("li"); 318 | $elem.className = "search-result search-result--" + result.result_type; 319 | $elem.dataset.href = url; 320 | $elem.setAttribute("title", result.full_name + " docs page"); 321 | 322 | var $title = document.createElement("div"); 323 | $title.setAttribute("class", "search-result__title"); 324 | var $titleLink = document.createElement("a"); 325 | $titleLink.setAttribute("href", url); 326 | 327 | $titleLink.innerHTML = title; 328 | $title.appendChild($titleLink); 329 | $elem.appendChild($title); 330 | $elem.addEventListener("click", function() { 331 | $titleLink.click(); 332 | }); 333 | 334 | if (result.result_type !== "type") { 335 | var $type = document.createElement("div"); 336 | $type.setAttribute("class", "search-result__type"); 337 | $type.innerHTML = query.highlight(result.type); 338 | $elem.appendChild($type); 339 | } 340 | 341 | if(result.summary){ 342 | var $doc = document.createElement("div"); 343 | $doc.setAttribute("class", "search-result__doc"); 344 | $doc.innerHTML = query.highlight(sanitize(result.summary)); 345 | $elem.appendChild($doc); 346 | } 347 | 348 | $elem.appendChild(document.createComment(JSON.stringify(result))); 349 | $frag.appendChild($elem); 350 | }); 351 | 352 | $resultsElem.appendChild($frag); 353 | 354 | CrystalDoc.toggleResultsList(true); 355 | }; 356 | 357 | CrystalDoc.toggleResultsList = function(visible) { 358 | if (visible) { 359 | document.querySelector(".types-list").classList.add("hidden"); 360 | document.querySelector(".search-results").classList.remove("hidden"); 361 | } else { 362 | document.querySelector(".types-list").classList.remove("hidden"); 363 | document.querySelector(".search-results").classList.add("hidden"); 364 | } 365 | }; 366 | 367 | CrystalDoc.Query = function(string) { 368 | this.original = string; 369 | this.terms = string.split(/\s+/).filter(function(word) { 370 | return CrystalDoc.Query.stripModifiers(word).length > 0; 371 | }); 372 | 373 | var normalized = this.terms.map(CrystalDoc.Query.normalizeTerm); 374 | this.normalizedTerms = normalized; 375 | 376 | function runMatcher(field, matcher) { 377 | if (!field) { 378 | return false; 379 | } 380 | var normalizedValue = CrystalDoc.Query.normalizeTerm(field); 381 | 382 | var matches = []; 383 | normalized.forEach(function(term) { 384 | if (matcher(normalizedValue, term)) { 385 | matches.push(term); 386 | } 387 | }); 388 | return matches.length > 0 ? matches : false; 389 | } 390 | 391 | this.matches = function(field) { 392 | return runMatcher(field, function(normalized, term) { 393 | if (term[0] == "#" || term[0] == ".") { 394 | return false; 395 | } 396 | return normalized.indexOf(term) >= 0; 397 | }); 398 | }; 399 | 400 | function namespaceMatcher(normalized, term){ 401 | var i = term.indexOf(":"); 402 | if(i >= 0){ 403 | term = term.replace(/^::?|::?$/, ""); 404 | var index = normalized.indexOf(term); 405 | if((index == 0) || (index > 0 && normalized[index-1] == ":")){ 406 | return true; 407 | } 408 | } 409 | return false; 410 | } 411 | this.matchesMethod = function(name, kind, type) { 412 | return runMatcher(name, function(normalized, term) { 413 | var i = term.indexOf("#"); 414 | if(i >= 0){ 415 | if (kind != "instance_method") { 416 | return false; 417 | } 418 | }else{ 419 | i = term.indexOf("."); 420 | if(i >= 0){ 421 | if (kind != "class_method" && kind != "macro" && kind != "constructor") { 422 | return false; 423 | } 424 | }else{ 425 | //neither # nor . 426 | if(term.indexOf(":") && namespaceMatcher(normalized, term)){ 427 | return true; 428 | } 429 | } 430 | } 431 | 432 | var methodName = term; 433 | if(i >= 0){ 434 | var termType = term.substring(0, i); 435 | methodName = term.substring(i+1); 436 | 437 | if(termType != "") { 438 | if(CrystalDoc.Query.normalizeTerm(type.full_name).indexOf(termType) < 0){ 439 | return false; 440 | } 441 | } 442 | } 443 | return normalized.indexOf(methodName) >= 0; 444 | }); 445 | }; 446 | 447 | this.matchesNamespace = function(namespace){ 448 | return runMatcher(namespace, namespaceMatcher); 449 | }; 450 | 451 | this.highlight = function(string) { 452 | if (typeof string == "undefined") { 453 | return ""; 454 | } 455 | function escapeRegExp(s) { 456 | return s.replace(/[.*+?\^${}()|\[\]\\]/g, "\\$&").replace(/^[#\.:]+/, ""); 457 | } 458 | return string.replace( 459 | new RegExp("(" + this.normalizedTerms.map(escapeRegExp).join("|") + ")", "gi"), 460 | "$1" 461 | ); 462 | }; 463 | }; 464 | CrystalDoc.Query.normalizeTerm = function(term) { 465 | return term.toLowerCase(); 466 | }; 467 | CrystalDoc.Query.stripModifiers = function(term) { 468 | switch (term[0]) { 469 | case "#": 470 | case ".": 471 | case ":": 472 | return term.substr(1); 473 | 474 | default: 475 | return term; 476 | } 477 | } 478 | 479 | CrystalDoc.search = function(string) { 480 | if(!CrystalDoc.searchIndex) { 481 | console.log("CrystalDoc search index not initialized, delaying search"); 482 | 483 | document.addEventListener("CrystalDoc:loaded", function listener(){ 484 | document.removeEventListener("CrystalDoc:loaded", listener); 485 | CrystalDoc.search(string); 486 | }); 487 | return; 488 | } 489 | 490 | document.dispatchEvent(new Event("CrystalDoc:searchStarted")); 491 | 492 | var query = new CrystalDoc.Query(string); 493 | var results = CrystalDoc.runQuery(query); 494 | results = CrystalDoc.rankResults(results, query); 495 | CrystalDoc.displaySearchResults(results, query); 496 | 497 | document.dispatchEvent(new Event("CrystalDoc:searchPerformed")); 498 | }; 499 | 500 | CrystalDoc.initializeIndex = function(data) { 501 | CrystalDoc.searchIndex = data; 502 | 503 | document.dispatchEvent(new Event("CrystalDoc:loaded")); 504 | }; 505 | 506 | CrystalDoc.loadIndex = function() { 507 | function loadJSON(file, callback) { 508 | var xobj = new XMLHttpRequest(); 509 | xobj.overrideMimeType("application/json"); 510 | xobj.open("GET", file, true); 511 | xobj.onreadystatechange = function() { 512 | if (xobj.readyState == 4 && xobj.status == "200") { 513 | callback(xobj.responseText); 514 | } 515 | }; 516 | xobj.send(null); 517 | } 518 | 519 | function loadScript(file) { 520 | script = document.createElement("script"); 521 | script.src = file; 522 | document.body.appendChild(script); 523 | } 524 | 525 | function parseJSON(json) { 526 | CrystalDoc.initializeIndex(JSON.parse(json)); 527 | } 528 | 529 | for(var i = 0; i < document.scripts.length; i++){ 530 | var script = document.scripts[i]; 531 | if (script.src && script.src.indexOf("js/doc.js") >= 0) { 532 | if (script.src.indexOf("file://") == 0) { 533 | // We need to support JSONP files for the search to work on local file system. 534 | var jsonPath = script.src.replace("js/doc.js", "search-index.js"); 535 | loadScript(jsonPath); 536 | return; 537 | } else { 538 | var jsonPath = script.src.replace("js/doc.js", "index.json"); 539 | loadJSON(jsonPath, parseJSON); 540 | return; 541 | } 542 | } 543 | } 544 | console.error("Could not find location of js/doc.js"); 545 | }; 546 | 547 | // Callback for jsonp 548 | function crystal_doc_search_index_callback(data) { 549 | CrystalDoc.initializeIndex(data); 550 | } 551 | 552 | Navigator = function(sidebar, searchInput, list, leaveSearchScope){ 553 | this.list = list; 554 | var self = this; 555 | 556 | var performingSearch = false; 557 | 558 | document.addEventListener('CrystalDoc:searchStarted', function(){ 559 | performingSearch = true; 560 | }); 561 | document.addEventListener('CrystalDoc:searchDebounceStarted', function(){ 562 | performingSearch = true; 563 | }); 564 | document.addEventListener('CrystalDoc:searchPerformed', function(){ 565 | performingSearch = false; 566 | }); 567 | document.addEventListener('CrystalDoc:searchDebounceStopped', function(event){ 568 | performingSearch = false; 569 | }); 570 | 571 | function delayWhileSearching(callback) { 572 | if(performingSearch){ 573 | document.addEventListener('CrystalDoc:searchPerformed', function listener(){ 574 | document.removeEventListener('CrystalDoc:searchPerformed', listener); 575 | 576 | // add some delay to let search results display kick in 577 | setTimeout(callback, 100); 578 | }); 579 | }else{ 580 | callback(); 581 | } 582 | } 583 | 584 | function clearMoveTimeout() { 585 | clearTimeout(self.moveTimeout); 586 | self.moveTimeout = null; 587 | } 588 | 589 | function startMoveTimeout(upwards){ 590 | /*if(self.moveTimeout) { 591 | clearMoveTimeout(); 592 | } 593 | 594 | var go = function() { 595 | if (!self.moveTimeout) return; 596 | self.move(upwards); 597 | self.moveTimout = setTimeout(go, 600); 598 | }; 599 | self.moveTimeout = setTimeout(go, 800);*/ 600 | } 601 | 602 | function scrollCenter(element) { 603 | var rect = element.getBoundingClientRect(); 604 | var middle = sidebar.clientHeight / 2; 605 | sidebar.scrollTop += rect.top + rect.height / 2 - middle; 606 | } 607 | 608 | var move = this.move = function(upwards){ 609 | if(!this.current){ 610 | this.highlightFirst(); 611 | return true; 612 | } 613 | var next = upwards ? this.current.previousElementSibling : this.current.nextElementSibling; 614 | if(next && next.classList) { 615 | this.highlight(next); 616 | scrollCenter(next); 617 | return true; 618 | } 619 | return false; 620 | }; 621 | 622 | this.moveRight = function(){ 623 | }; 624 | this.moveLeft = function(){ 625 | }; 626 | 627 | this.highlight = function(elem) { 628 | if(!elem){ 629 | return; 630 | } 631 | this.removeHighlight(); 632 | 633 | this.current = elem; 634 | this.current.classList.add("current"); 635 | }; 636 | 637 | this.highlightFirst = function(){ 638 | this.highlight(this.list.querySelector('li:first-child')); 639 | }; 640 | 641 | this.removeHighlight = function() { 642 | if(this.current){ 643 | this.current.classList.remove("current"); 644 | } 645 | this.current = null; 646 | } 647 | 648 | this.openSelectedResult = function() { 649 | if(this.current) { 650 | this.current.click(); 651 | } 652 | } 653 | 654 | this.focus = function() { 655 | searchInput.focus(); 656 | searchInput.select(); 657 | this.highlightFirst(); 658 | } 659 | 660 | function handleKeyUp(event) { 661 | switch(event.key) { 662 | case "ArrowUp": 663 | case "ArrowDown": 664 | case "i": 665 | case "j": 666 | case "k": 667 | case "l": 668 | case "c": 669 | case "h": 670 | case "t": 671 | case "n": 672 | event.stopPropagation(); 673 | clearMoveTimeout(); 674 | } 675 | } 676 | 677 | function handleKeyDown(event) { 678 | switch(event.key) { 679 | case "Enter": 680 | event.stopPropagation(); 681 | event.preventDefault(); 682 | leaveSearchScope(); 683 | self.openSelectedResult(); 684 | break; 685 | case "Escape": 686 | event.stopPropagation(); 687 | event.preventDefault(); 688 | leaveSearchScope(); 689 | break; 690 | case "j": 691 | case "c": 692 | case "ArrowUp": 693 | if(event.ctrlKey || event.key == "ArrowUp") { 694 | event.stopPropagation(); 695 | self.move(true); 696 | startMoveTimeout(true); 697 | } 698 | break; 699 | case "k": 700 | case "h": 701 | case "ArrowDown": 702 | if(event.ctrlKey || event.key == "ArrowDown") { 703 | event.stopPropagation(); 704 | self.move(false); 705 | startMoveTimeout(false); 706 | } 707 | break; 708 | case "k": 709 | case "t": 710 | case "ArrowLeft": 711 | if(event.ctrlKey || event.key == "ArrowLeft") { 712 | event.stopPropagation(); 713 | self.moveLeft(); 714 | } 715 | break; 716 | case "l": 717 | case "n": 718 | case "ArrowRight": 719 | if(event.ctrlKey || event.key == "ArrowRight") { 720 | event.stopPropagation(); 721 | self.moveRight(); 722 | } 723 | break; 724 | } 725 | } 726 | 727 | function handleInputKeyUp(event) { 728 | switch(event.key) { 729 | case "ArrowUp": 730 | case "ArrowDown": 731 | event.stopPropagation(); 732 | event.preventDefault(); 733 | clearMoveTimeout(); 734 | } 735 | } 736 | 737 | function handleInputKeyDown(event) { 738 | switch(event.key) { 739 | case "Enter": 740 | event.stopPropagation(); 741 | event.preventDefault(); 742 | delayWhileSearching(function(){ 743 | self.openSelectedResult(); 744 | leaveSearchScope(); 745 | }); 746 | break; 747 | case "Escape": 748 | event.stopPropagation(); 749 | event.preventDefault(); 750 | // remove focus from search input 751 | leaveSearchScope(); 752 | sidebar.focus(); 753 | break; 754 | case "ArrowUp": 755 | event.stopPropagation(); 756 | event.preventDefault(); 757 | self.move(true); 758 | startMoveTimeout(true); 759 | break; 760 | 761 | case "ArrowDown": 762 | event.stopPropagation(); 763 | event.preventDefault(); 764 | self.move(false); 765 | startMoveTimeout(false); 766 | break; 767 | } 768 | } 769 | 770 | sidebar.tabIndex = 100; // set tabIndex to enable keylistener 771 | sidebar.addEventListener('keyup', function(event) { 772 | handleKeyUp(event); 773 | }); 774 | sidebar.addEventListener('keydown', function(event) { 775 | handleKeyDown(event); 776 | }); 777 | searchInput.addEventListener('keydown', function(event) { 778 | handleInputKeyDown(event); 779 | }); 780 | searchInput.addEventListener('keyup', function(event) { 781 | handleInputKeyUp(event); 782 | }); 783 | this.move(); 784 | }; 785 | 786 | var UsageModal = function(title, content) { 787 | var $body = document.body; 788 | var self = this; 789 | var $modalBackground = document.createElement("div"); 790 | $modalBackground.classList.add("modal-background"); 791 | var $usageModal = document.createElement("div"); 792 | $usageModal.classList.add("usage-modal"); 793 | $modalBackground.appendChild($usageModal); 794 | var $title = document.createElement("h3"); 795 | $title.classList.add("modal-title"); 796 | $title.innerHTML = title 797 | $usageModal.appendChild($title); 798 | var $closeButton = document.createElement("span"); 799 | $closeButton.classList.add("close-button"); 800 | $closeButton.setAttribute("title", "Close modal"); 801 | $closeButton.innerText = '×'; 802 | $usageModal.appendChild($closeButton); 803 | $usageModal.insertAdjacentHTML("beforeend", content); 804 | 805 | $modalBackground.addEventListener('click', function(event) { 806 | var element = event.target || event.srcElement; 807 | 808 | if(element == $modalBackground) { 809 | self.hide(); 810 | } 811 | }); 812 | $closeButton.addEventListener('click', function(event) { 813 | self.hide(); 814 | }); 815 | 816 | $body.insertAdjacentElement('beforeend', $modalBackground); 817 | 818 | this.show = function(){ 819 | $body.classList.add("js-modal-visible"); 820 | }; 821 | this.hide = function(){ 822 | $body.classList.remove("js-modal-visible"); 823 | }; 824 | this.isVisible = function(){ 825 | return $body.classList.contains("js-modal-visible"); 826 | } 827 | } 828 | 829 | 830 | document.addEventListener('DOMContentLoaded', function() { 831 | var sessionStorage; 832 | try { 833 | sessionStorage = window.sessionStorage; 834 | } catch (e) { } 835 | if(!sessionStorage) { 836 | sessionStorage = { 837 | setItem: function() {}, 838 | getItem: function() {}, 839 | removeItem: function() {} 840 | }; 841 | } 842 | 843 | var repositoryName = document.querySelector('#repository-name').getAttribute('content'); 844 | var typesList = document.querySelector('.types-list'); 845 | var searchInput = document.querySelector('.search-input'); 846 | var parents = document.querySelectorAll('.types-list li.parent'); 847 | 848 | var scrollSidebarToOpenType = function(){ 849 | var openTypes = typesList.querySelectorAll('.current'); 850 | if (openTypes.length > 0) { 851 | var lastOpenType = openTypes[openTypes.length - 1]; 852 | lastOpenType.scrollIntoView(); 853 | } 854 | } 855 | 856 | scrollSidebarToOpenType(); 857 | 858 | var setPersistentSearchQuery = function(value){ 859 | sessionStorage.setItem(repositoryName + '::search-input:value', value); 860 | } 861 | 862 | for(var i = 0; i < parents.length; i++) { 863 | var _parent = parents[i]; 864 | _parent.addEventListener('click', function(e) { 865 | e.stopPropagation(); 866 | 867 | if(e.target.tagName.toLowerCase() == 'li') { 868 | if(e.target.className.match(/open/)) { 869 | sessionStorage.removeItem(e.target.getAttribute('data-id')); 870 | e.target.className = e.target.className.replace(/ +open/g, ''); 871 | } else { 872 | sessionStorage.setItem(e.target.getAttribute('data-id'), '1'); 873 | if(e.target.className.indexOf('open') == -1) { 874 | e.target.className += ' open'; 875 | } 876 | } 877 | } 878 | }); 879 | 880 | if(sessionStorage.getItem(_parent.getAttribute('data-id')) == '1') { 881 | _parent.className += ' open'; 882 | } 883 | } 884 | 885 | var leaveSearchScope = function(){ 886 | CrystalDoc.toggleResultsList(false); 887 | window.focus(); 888 | } 889 | 890 | var navigator = new Navigator(document.querySelector('.types-list'), searchInput, document.querySelector(".search-results"), leaveSearchScope); 891 | 892 | CrystalDoc.loadIndex(); 893 | var searchTimeout; 894 | var lastSearchText = false; 895 | var performSearch = function() { 896 | document.dispatchEvent(new Event("CrystalDoc:searchDebounceStarted")); 897 | 898 | clearTimeout(searchTimeout); 899 | searchTimeout = setTimeout(function() { 900 | var text = searchInput.value; 901 | 902 | if(text == "") { 903 | CrystalDoc.toggleResultsList(false); 904 | }else if(text == lastSearchText){ 905 | document.dispatchEvent(new Event("CrystalDoc:searchDebounceStopped")); 906 | }else{ 907 | CrystalDoc.search(text); 908 | navigator.highlightFirst(); 909 | searchInput.focus(); 910 | } 911 | lastSearchText = text; 912 | setPersistentSearchQuery(text); 913 | }, 200); 914 | }; 915 | 916 | if(location.hash.length > 3 && location.hash.substring(0,3) == "#q="){ 917 | // allows directly linking a search query which is then executed on the client 918 | // this comes handy for establishing a custom browser search engine with https://crystal-lang.org/api/#q=%s as a search URL 919 | // TODO: Add OpenSearch description 920 | var searchQuery = location.hash.substring(3); 921 | history.pushState({searchQuery: searchQuery}, "Search for " + searchQuery, location.href.replace(/#q=.*/, "")); 922 | searchInput.value = searchQuery; 923 | document.addEventListener('CrystalDoc:loaded', performSearch); 924 | } 925 | 926 | if (searchInput.value.length == 0) { 927 | var searchText = sessionStorage.getItem(repositoryName + '::search-input:value'); 928 | if(searchText){ 929 | searchInput.value = searchText; 930 | } 931 | } 932 | searchInput.addEventListener('keyup', performSearch); 933 | searchInput.addEventListener('input', performSearch); 934 | 935 | var usageModal = new UsageModal('Keyboard Shortcuts', '' + 936 | '' 974 | ); 975 | 976 | function handleShortkeys(event) { 977 | var element = event.target || event.srcElement; 978 | 979 | if(element.tagName == "INPUT" || element.tagName == "TEXTAREA" || element.parentElement.tagName == "TEXTAREA"){ 980 | return; 981 | } 982 | 983 | switch(event.key) { 984 | case "?": 985 | usageModal.show(); 986 | break; 987 | 988 | case "Escape": 989 | usageModal.hide(); 990 | break; 991 | 992 | case "s": 993 | case "/": 994 | if(usageModal.isVisible()) { 995 | return; 996 | } 997 | event.stopPropagation(); 998 | navigator.focus(); 999 | performSearch(); 1000 | break; 1001 | } 1002 | } 1003 | 1004 | document.addEventListener('keyup', handleShortkeys); 1005 | 1006 | var scrollToEntryFromLocationHash = function() { 1007 | var hash = window.location.hash; 1008 | if (hash) { 1009 | var targetAnchor = unescape(hash.substr(1)); 1010 | var targetEl = document.querySelectorAll('.entry-detail[id="' + targetAnchor + '"]'); 1011 | 1012 | if (targetEl && targetEl.length > 0) { 1013 | targetEl[0].offsetParent.scrollTop = targetEl[0].offsetTop; 1014 | } 1015 | } 1016 | }; 1017 | window.addEventListener("hashchange", scrollToEntryFromLocationHash, false); 1018 | scrollToEntryFromLocationHash(); 1019 | }); 1020 | -------------------------------------------------------------------------------- /docs/search-index.js: -------------------------------------------------------------------------------- 1 | crystal_doc_search_index_callback({"repository_name":"github.com/madeindjs/Crystagiri","body":"# Crystagiri\n\nAn HTML parser library for Crystal like the amazing [Nokogiri](https://github.com/sparklemotion/nokogiri) Ruby gem.\n\n> I won't pretend that **Crystagiri** does much as **Nokogiri**. All help is welcome! :)\n\n## Installation\n\nAdd this to your application's `shard.yml`:\n\n```yaml\ndependencies:\n crystagiri:\n github: madeindjs/crystagiri\n```\n\nand then run\n\n```bash\n$ crystal deps\n```\n\n## Usage\n\n```crystal\nrequire \"crystagiri\"\n```\n\nThen you can simply instantiate a `Crystagiri::HTML` object from an HTML `String` like this\n\n```crystal\ndoc = Crystagiri::HTML.new \"

Crystagiri is awesome!!

\"\n```\n\n... or directly load it from a Web URL or a pathname:\n\n```crystal\ndoc = Crystagiri::HTML.from_file \"README.md\"\ndoc = Crystagiri::HTML.from_url \"http://example.com/\"\n```\n\n> Also you can specify `follow: true` flag if you want to follow redirect URL\n\nThen you can search all [`XML::Node`](https://crystal-lang.org/api/XML/Node.html)s from the `Crystagiri::HTML` instance. The tags found will be `Crystagiri::Tag` objects with the `.node` property:\n\n* CSS query\n\n```Crystal\nputs doc.css(\"li > strong.title\") { |tag| puts tag.node}\n# => .. \n# => .. \n```\n\n> **Known limitations**: Currently, you can't use CSS queries with complex search specifiers like `:nth-child`\n\n* HTML tag\n\n```Crystal\ndoc.where_tag(\"h2\") { |tag| puts tag.content }\n# => Development\n# => Contributing\n```\n\n* HTML id\n\n```Crystal\nputs doc.at_id(\"main-content\").tagname\n# => div\n```\n\n* HTML class attribute\n\n```Crystal\ndoc.where_class(\"summary\") { |tag| puts tag.node }\n# =>
..
\n# =>
..
\n# =>
..
\n```\n\n## Benchmark\n\nI know you love benchmarks between **Ruby** & **Crystal**, so here's one:\n\n```ruby\nrequire \"nokogiri\"\nt1 = Time.now\ndoc = Nokogiri::HTML File.read(\"spec/fixture/HTML.html\")\n1..100000.times do\n doc.at_css(\"h1\")\n doc.css(\".step-title\"){ |tag| tag }\nend\nputs \"executed in #{Time.now - t1} milliseconds\"\n```\n\n> executed in 00:00:11.10 seconds with Ruby 2.6.0 with RVM on old Mac\n\n```crystal\nrequire \"crystagiri\"\nt = Time.now\ndoc = Crystagiri::HTML.from_file \"./spec/fixture/HTML.html\"\n1..100000.times do\n doc.at_css(\"h1\")\n doc.css(\".step-title\") { |tag| tag }\nend\nputs \"executed in #{Time.now - t} milliseconds\"\n```\n\n> executed in 00:00:03.09 seconds on Crystal 0.27.2 on LLVM 6.0.1 with release flag\n\nCrystagiri is more than **two time faster** than Nokogiri!!\n\n\n## Development\n\nClone this repository and navigate to it:\n\n```bash\n$ git clone https://github.com/madeindjs/crystagiri.git\n$ cd crystagiri\n```\n\nYou can generate all documentation with\n\n```bash\n$ crystal doc\n```\n\nAnd run **spec** tests to ensure everything works correctly\n\n```bash\n$ crystal spec\n```\n\n\n## Contributing\n\n1. Fork it ( https://github.com/madeindjs/crystagiri/fork )\n2. Create your feature branch `git checkout -b my-new-feature`\n3. Commit your changes `git commit -am \"Add some feature\"`\n4. Push to the branch `git push origin my-new-feature`\n5. Create a new Pull Request\n\n## Contributors\n\nSee the [list on Github](https://github.com/madeindjs/Crystagiri/graphs/contributors)\n","program":{"html_id":"github.com/madeindjs/Crystagiri/toplevel","path":"toplevel.html","kind":"module","full_name":"Top Level Namespace","name":"Top Level Namespace","abstract":false,"superclass":null,"ancestors":[],"locations":[],"repository_name":"github.com/madeindjs/Crystagiri","program":true,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":null,"doc":null,"summary":null,"class_methods":[],"constructors":[],"instance_methods":[],"macros":[],"types":[{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","path":"Crystagiri.html","kind":"module","full_name":"Crystagiri","name":"Crystagiri","abstract":false,"superclass":null,"ancestors":[],"locations":[{"filename":"crystagiri/html.cr","line_number":4,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr"},{"filename":"crystagiri/tag.cr","line_number":1,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr"},{"filename":"crystagiri/version.cr","line_number":1,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/version.cr"},{"filename":"crystagiri.cr","line_number":3,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[{"id":"VERSION","name":"VERSION","value":"\"0.4.0-alpha\"","doc":null,"summary":null}],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":null,"doc":null,"summary":null,"class_methods":[],"constructors":[],"instance_methods":[],"macros":[],"types":[{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri/HTML","path":"Crystagiri/HTML.html","kind":"class","full_name":"Crystagiri::HTML","name":"HTML","abstract":false,"superclass":{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},"ancestors":[{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},{"html_id":"github.com/madeindjs/Crystagiri/Object","kind":"class","full_name":"Object","name":"Object"}],"locations":[{"filename":"crystagiri/html.cr","line_number":6,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","kind":"module","full_name":"Crystagiri","name":"Crystagiri"},"doc":"Represent an Html document who can be parsed","summary":"

Represent an Html document who can be parsed

","class_methods":[{"id":"css_query_to_xpath(query:String):String-class-method","html_id":"css_query_to_xpath(query:String):String-class-method","name":"css_query_to_xpath","doc":"Transform the css query into an xpath query","summary":"

Transform the css query into an xpath query

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String) : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L34","def":{"name":"css_query_to_xpath","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"query = \"//#{query}\"\nquery = query.gsub(/\\#([A-z0-9]+-*_*)+/) do |m|\n \"*[@id=\\\"%s\\\"]\" % (m.delete('#'))\nend\nquery = query.gsub(/\\.([A-z0-9]+-*_*)+/) do |m|\n \"[@class=\\\"%s\\\"]\" % (m.delete('.'))\nend\nquery = query.gsub(/\\s*>\\s*/) do |m|\n \"/\"\nend\nquery = query.gsub(\" \", \"//\")\nquery = query.gsub(/\\/\\[/) do |m|\n \"/*[\"\nend\nreturn query\n"}}],"constructors":[{"id":"from_file(path:String):HTML-class-method","html_id":"from_file(path:String):HTML-class-method","name":"from_file","doc":"Initialize an Html object from content of file\ndesigned by the given filepath","summary":"

Initialize an Html object from content of file designed by the given filepath

","abstract":false,"args":[{"name":"path","doc":null,"default_value":"","external_name":"path","restriction":"String"}],"args_string":"(path : String) : HTML","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L29","def":{"name":"from_file","args":[{"name":"path","doc":null,"default_value":"","external_name":"path","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"HTML","visibility":"Public","body":"return HTML.new(File.read(path))"}},{"id":"from_url(url:String,follow:Bool=false):HTML-class-method","html_id":"from_url(url:String,follow:Bool=false):HTML-class-method","name":"from_url","doc":"Initialize an Html object from Html source fetched\nfrom the url","summary":"

Initialize an Html object from Html source fetched from the url

","abstract":false,"args":[{"name":"url","doc":null,"default_value":"","external_name":"url","restriction":"String"},{"name":"follow","doc":null,"default_value":"false","external_name":"follow","restriction":"Bool"}],"args_string":"(url : String, follow : Bool = false) : HTML","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L12","def":{"name":"from_url","args":[{"name":"url","doc":null,"default_value":"","external_name":"url","restriction":"String"},{"name":"follow","doc":null,"default_value":"false","external_name":"follow","restriction":"Bool"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"HTML","visibility":"Public","body":"begin\n response = HTTP::Client.get(url)\n if response.status_code == 200\n return HTML.new(response.body)\n else\n if follow && (response.status_code == 301)\n from_url(response.headers[\"Location\"], follow: true)\n else\n raise(ArgumentError.new(\"Host returned #{response.status_code}\"))\n end\n end\nrescue Socket::Error\n raise(Socket::Error.new(\"Host #{url} cannot be fetched\"))\nend"}},{"id":"new(content:String)-class-method","html_id":"new(content:String)-class-method","name":"new","doc":"Initialize an Html object from Html source","summary":"

Initialize an Html object from Html source

","abstract":false,"args":[{"name":"content","doc":null,"default_value":"","external_name":"content","restriction":"String"}],"args_string":"(content : String)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L50","def":{"name":"new","args":[{"name":"content","doc":null,"default_value":"","external_name":"content","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"_ = allocate\n_.initialize(content)\nif _.responds_to?(:finalize)\n ::GC.add_finalizer(_)\nend\n_\n"}}],"instance_methods":[{"id":"at_css(query:String)-instance-method","html_id":"at_css(query:String)-instance-method","name":"at_css","doc":"Find first node corresponding to the css query and return\n`Crystagiri::Tag` if founded or a nil if not founded","summary":"

Find first node corresponding to the css query and return Crystagiri::Tag if founded or a nil if not founded

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L159","def":{"name":"at_css","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"css(query) do |tag|\n return tag\nend\nreturn nil\n"}},{"id":"at_id(id_name:String):Crystagiri::Tag?-instance-method","html_id":"at_id(id_name:String):Crystagiri::Tag?-instance-method","name":"at_id","doc":"Find a node by its id and return a\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find a node by its id and return a Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"id_name","doc":null,"default_value":"","external_name":"id_name","restriction":"String"}],"args_string":"(id_name : String) : Crystagiri::Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L140","def":{"name":"at_id","args":[{"name":"id_name","doc":null,"default_value":"","external_name":"id_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Crystagiri::Tag | Nil","visibility":"Public","body":"if node = @ids[id_name]?\n return (Tag.new(node)).as(Crystagiri::Tag)\nend"}},{"id":"at_tag(tag_name:String):Crystagiri::Tag?-instance-method","html_id":"at_tag(tag_name:String):Crystagiri::Tag?-instance-method","name":"at_tag","doc":"Find first tag by tag name and return\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find first tag by tag name and return Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"args_string":"(tag_name : String) : Crystagiri::Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L101","def":{"name":"at_tag","args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Crystagiri::Tag | Nil","visibility":"Public","body":"if tags = @tags[tag_name]?\n tags.each do |tag|\n return (Tag.new(tag)).as(Crystagiri::Tag)\n end\nend\nreturn nil\n"}},{"id":"content:String-instance-method","html_id":"content:String-instance-method","name":"content","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L7","def":{"name":"content","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@content"}},{"id":"css(query:String,&block):Array(Tag)-instance-method","html_id":"css(query:String,&block):Array(Tag)-instance-method","name":"css","doc":"Find all node corresponding to the css query and yield\n`Crystagiri::Tag` founded or a nil if not founded","summary":"

Find all node corresponding to the css query and yield Crystagiri::Tag founded or a nil if not founded

","abstract":false,"args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"args_string":"(query : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L148","def":{"name":"css","args":[{"name":"query","doc":null,"default_value":"","external_name":"query","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"query = HTML.css_query_to_xpath(query)\nreturn (@nodes.xpath_nodes(\"//#{query}\")).map do |node|\n tag = (Tag.new(node)).as(Crystagiri::Tag)\n yield tag\n tag\nend\n"}},{"id":"nodes:XML::Node-instance-method","html_id":"nodes:XML::Node-instance-method","name":"nodes","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : XML::Node","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L8","def":{"name":"nodes","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@nodes"}},{"id":"where_class(class_name:String,&block):Array(Tag)-instance-method","html_id":"where_class(class_name:String,&block):Array(Tag)-instance-method","name":"where_class","doc":"Find all nodes by classname and yield\n`Crystagiri::Tag` founded","summary":"

Find all nodes by classname and yield Crystagiri::Tag founded

","abstract":false,"args":[{"name":"class_name","doc":null,"default_value":"","external_name":"class_name","restriction":"String"}],"args_string":"(class_name : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L126","def":{"name":"where_class","args":[{"name":"class_name","doc":null,"default_value":"","external_name":"class_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"arr = [] of Crystagiri::Tag\nif klasses = @classes[class_name]?\n klasses.each do |node|\n klass = (Tag.new(node)).as(Crystagiri::Tag)\n yield klass\n arr << klass\n end\nend\nreturn arr\n"}},{"id":"where_tag(tag_name:String,&block):Array(Tag)-instance-method","html_id":"where_tag(tag_name:String,&block):Array(Tag)-instance-method","name":"where_tag","doc":"Find all nodes by tag name and yield\n`Crystagiri::Tag` founded","summary":"

Find all nodes by tag name and yield Crystagiri::Tag founded

","abstract":false,"args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"args_string":"(tag_name : String, &block) : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/html.cr#L112","def":{"name":"where_tag","args":[{"name":"tag_name","doc":null,"default_value":"","external_name":"tag_name","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":1,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"arr = [] of Crystagiri::Tag\nif tags = @tags[tag_name]?\n tags.each do |node|\n tag = (Tag.new(node)).as(Crystagiri::Tag)\n yield tag\n arr << tag\n end\nend\nreturn arr\n"}}],"macros":[],"types":[]},{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri/Tag","path":"Crystagiri/Tag.html","kind":"class","full_name":"Crystagiri::Tag","name":"Tag","abstract":false,"superclass":{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},"ancestors":[{"html_id":"github.com/madeindjs/Crystagiri/Reference","kind":"class","full_name":"Reference","name":"Reference"},{"html_id":"github.com/madeindjs/Crystagiri/Object","kind":"class","full_name":"Object","name":"Object"}],"locations":[{"filename":"crystagiri/tag.cr","line_number":2,"url":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr"}],"repository_name":"github.com/madeindjs/Crystagiri","program":false,"enum":false,"alias":false,"aliased":"","const":false,"constants":[],"included_modules":[],"extended_modules":[],"subclasses":[],"including_types":[],"namespace":{"html_id":"github.com/madeindjs/Crystagiri/Crystagiri","kind":"module","full_name":"Crystagiri","name":"Crystagiri"},"doc":null,"summary":null,"class_methods":[],"constructors":[{"id":"new(node:XML::Node)-class-method","html_id":"new(node:XML::Node)-class-method","name":"new","doc":null,"summary":null,"abstract":false,"args":[{"name":"node","doc":null,"default_value":"","external_name":"node","restriction":"XML::Node"}],"args_string":"(node : XML::Node)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L5","def":{"name":"new","args":[{"name":"node","doc":null,"default_value":"","external_name":"node","restriction":"XML::Node"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"_ = allocate\n_.initialize(node)\nif _.responds_to?(:finalize)\n ::GC.add_finalizer(_)\nend\n_\n"}}],"instance_methods":[{"id":"children:Array(Tag)-instance-method","html_id":"children:Array(Tag)-instance-method","name":"children","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : Array(Tag)","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L27","def":{"name":"children","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Array(Tag)","visibility":"Public","body":"children = [] of Tag\n@node.children.each do |node|\n if node.element?\n children << (Tag.new(node))\n end\nend\nchildren\n"}},{"id":"classname:String?-instance-method","html_id":"classname:String?-instance-method","name":"classname","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L8","def":{"name":"classname","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String | Nil","visibility":"Public","body":"return @node[\"class\"]? ? @node[\"class\"] : nil"}},{"id":"content:String-instance-method","html_id":"content:String-instance-method","name":"content","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L16","def":{"name":"content","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"return @node.text != nil ? @node.text.as(String) : \"\".as(String)"}},{"id":"has_class?(klass:String):Bool-instance-method","html_id":"has_class?(klass:String):Bool-instance-method","name":"has_class?","doc":null,"summary":null,"abstract":false,"args":[{"name":"klass","doc":null,"default_value":"","external_name":"klass","restriction":"String"}],"args_string":"(klass : String) : Bool","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L37","def":{"name":"has_class?","args":[{"name":"klass","doc":null,"default_value":"","external_name":"klass","restriction":"String"}],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Bool","visibility":"Public","body":"if classes = classname\n return classes.includes?(klass)\nend\nfalse\n"}},{"id":"node:XML::Node-instance-method","html_id":"node:XML::Node-instance-method","name":"node","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : XML::Node","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L3","def":{"name":"node","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"","visibility":"Public","body":"@node"}},{"id":"parent:Tag?-instance-method","html_id":"parent:Tag?-instance-method","name":"parent","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : Tag?","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L20","def":{"name":"parent","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"Tag | Nil","visibility":"Public","body":"if parent = @node.parent\n return Tag.new(parent)\nend\nnil\n"}},{"id":"tagname:String-instance-method","html_id":"tagname:String-instance-method","name":"tagname","doc":null,"summary":null,"abstract":false,"args":[],"args_string":" : String","source_link":"https://github.com/madeindjs/Crystagiri/blob/6efa7a429e7fc69c3bcbee1d44015fccbe485486/src/crystagiri/tag.cr#L12","def":{"name":"tagname","args":[],"double_splat":null,"splat_index":null,"yields":null,"block_arg":null,"return_type":"String","visibility":"Public","body":"return @node.name"}}],"macros":[],"types":[]}]}]}}) -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: crystagiri 2 | version: 0.3.4 3 | 4 | authors: 5 | - Alexandre Rousseau 6 | - prutheus 7 | 8 | crystal: 0.20.1 9 | 10 | license: MIT 11 | -------------------------------------------------------------------------------- /spec/crystagiri_html_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Crystagiri::HTML do 4 | it "should instanciate an HTML object" do 5 | doc = Crystagiri::HTML.new "

Hello

" 6 | doc.should_not be_nil 7 | end 8 | 9 | it "should instanciate an HTML object from a given filepath" do 10 | doc = Crystagiri::HTML.from_file "README.md" 11 | doc.should be_a Crystagiri::HTML 12 | doc.content.should_not eq "" 13 | end 14 | 15 | it "should instanciate an HTML object from a website url" do 16 | doc = Crystagiri::HTML.from_url "http://example.com/" 17 | doc.should be_a Crystagiri::HTML 18 | doc.content.should_not eq "" 19 | end 20 | 21 | it "should not instanciate an HTML object from a website url which responds 301" do 22 | expect_raises(ArgumentError) do 23 | doc = Crystagiri::HTML.from_url "http://google.com/" 24 | end 25 | end 26 | 27 | it "should instanciate an HTML object from a website url which responds 301 using follow: true" do 28 | doc = Crystagiri::HTML.from_url "http://google.com/", follow: true 29 | doc.should be_a Crystagiri::HTML 30 | doc.content.should_not eq "" 31 | end 32 | 33 | it "should find nodes by tag name" do 34 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 35 | doc.should be_a Crystagiri::HTML 36 | # Count number of tags founded 37 | {"body" => 1, "h2" => 2, "strong" => 8}.each do |tag, qty| 38 | nb_tag = 0 39 | doc.where_tag(tag) { |i| nb_tag += 1 } 40 | nb_tag.should eq qty 41 | end 42 | end 43 | 44 | it "should find by classname name" do 45 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 46 | # Count number of tags founded 47 | {"step-title" => 8, "introduction" => 2, "steps" => 2}.each do |classname, qty| 48 | nb_tag = 0 49 | doc.where_class(classname) { |i| nb_tag += 1 } 50 | nb_tag.should eq qty 51 | end 52 | end 53 | 54 | it "should find by id" do 55 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 56 | doc.at_id("main-content").should be_a Crystagiri::Tag 57 | end 58 | 59 | it "should find 'tag+classname' by css query" do 60 | Crystagiri::HTML.css_query_to_xpath("a.method-permalink").should eq "//a[@class=\"method-permalink\"]" 61 | Crystagiri::HTML.css_query_to_xpath("strong.step-title").should eq "//strong[@class=\"step-title\"]" 62 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 63 | doc.at_css("strong.step-title").should be_a Crystagiri::Tag 64 | end 65 | 66 | it "should find 'id' by css query" do 67 | # test query converter 68 | css_query = "#main-content" 69 | xpath_query = "//*[@id=\"main-content\"]" 70 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 71 | # test on local file 72 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 73 | doc.at_css(css_query).should be_a Crystagiri::Tag 74 | end 75 | 76 | it "should find 'id' by css query" do 77 | # test query converter 78 | css_query = "#main-content" 79 | xpath_query = "//*[@id=\"main-content\"]" 80 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 81 | # test on local file 82 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 83 | doc.at_css(css_query).should be_a Crystagiri::Tag 84 | end 85 | 86 | it "should find subtags like 'id tag.class' by css query" do 87 | # test query converter 88 | css_query = "#main-content ol.steps" 89 | xpath_query = "//*[@id=\"main-content\"]//ol[@class=\"steps\"]" 90 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 91 | # test on local file 92 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 93 | doc.at_css(css_query).should be_a Crystagiri::Tag 94 | # 95 | # another test 96 | css_query = "ol.steps strong.step-title" 97 | xpath_query = "//ol[@class=\"steps\"]//strong[@class=\"step-title\"]" 98 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 99 | doc.at_css(css_query).should be_a Crystagiri::Tag 100 | end 101 | 102 | it "should find only first subtags like 'id > tag.class' by css query" do 103 | # test query converter 104 | css_query = "#main-content > strong.step-title" 105 | xpath_query = "//*[@id=\"main-content\"]/strong[@class=\"step-title\"]" 106 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 107 | # test on local file 108 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 109 | doc.at_css(css_query).should be_nil 110 | # 111 | # another test 112 | css_query = "body>quote.introduction" 113 | xpath_query = "//body/quote[@class=\"introduction\"]" 114 | Crystagiri::HTML.css_query_to_xpath(css_query).should eq xpath_query 115 | doc.at_css(css_query).should be_a Crystagiri::Tag 116 | end 117 | 118 | it "should find a class with number" do 119 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 120 | doc.at_css(".title69").should be_a Crystagiri::Tag 121 | doc.css("#ctl00_ContentPlaceHolder_LblRecetteNombre") { |tag| 122 | tag.content.should eq "4 pers" 123 | } 124 | end 125 | 126 | it "should cess return an array" do 127 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 128 | doc.css("li") { |t| t }.size.should eq 8 129 | end 130 | 131 | it "should pass bulletprrof property name" do 132 | Crystagiri::HTML.css_query_to_xpath("strong #Az_xA--az.--").should eq "//strong//*[@id=\"Az_xA--az\"].--" 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /spec/crystagiri_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Crystagiri do 4 | it "should have a version number" do 5 | Crystagiri::VERSION.should_not be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/crystagiri_tag_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe Crystagiri::Tag do 4 | it "should instanciate a Tag object" do 5 | doc = Crystagiri::HTML.from_file "README.md" 6 | doc.where_tag("strong") { |tag| 7 | tag.should be_a Crystagiri::Tag 8 | tag.node.should be_a XML::Node 9 | } 10 | end 11 | 12 | it "should instanciate a Tag object" do 13 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 14 | # Check data for first strong tag 15 | tag = doc.at_tag("strong").as(Crystagiri::Tag) 16 | tag.content.should eq "Clone" 17 | tag.tagname.should eq "strong" 18 | tag.classname.should eq "step-title" 19 | # Check data for an another tag 20 | tag = doc.at_tag("h1").as(Crystagiri::Tag) 21 | tag.content.should eq "Crystagiri" 22 | tag.tagname.should eq "h1" 23 | tag.classname.should eq nil 24 | end 25 | 26 | it "can browse document using only Tag objects: parents" do 27 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 28 | tag = doc.at_tag("strong").as(Crystagiri::Tag) 29 | 30 | parent = tag.parent.as(Crystagiri::Tag) 31 | parent.should_not be nil 32 | parent.tagname.should eq "li" 33 | 34 | pparent = parent.parent.as(Crystagiri::Tag) 35 | pparent.tagname.should eq "ol" 36 | pparent.classname.should eq "steps" 37 | end 38 | 39 | it "can browse document using only Tag objects: children" do 40 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 41 | tag = doc.at_id("main-content").as(Crystagiri::Tag) 42 | 43 | children = tag.children.as(Array(Crystagiri::Tag)) 44 | children.size.should eq 5 45 | 46 | children.each do |child| 47 | child.should be_a Crystagiri::Tag 48 | end 49 | 50 | h2 = children[0] 51 | h2.tagname.should eq "h2" 52 | h2.content.should eq "Developement" 53 | 54 | h2.children.should eq [] of Crystagiri::Tag 55 | 56 | ol = children[3] 57 | li = ol.children[0] 58 | strong = li.children[0] 59 | a = strong.children[0] 60 | 61 | a.should be_a Crystagiri::Tag 62 | a.tagname.should eq "a" 63 | a.content.should eq "Fork it" 64 | end 65 | 66 | it "can detect wether a tag has a particular class" do 67 | doc = Crystagiri::HTML.from_file "spec/fixture/HTML.html" 68 | tag = doc.at_id("main-content").as(Crystagiri::Tag) 69 | 70 | children = tag.children.as(Array(Crystagiri::Tag)) 71 | 72 | ol = children[3] 73 | li = ol.children[0] 74 | strong = li.children[0] 75 | 76 | strong.has_class?("step-title").should eq true 77 | strong.has_class?("first").should eq true 78 | strong.has_class?("dummy").should eq false 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/fixture/HTML.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Crystagiri 5 | 6 | 7 |

Crystagiri

8 |

An Html parser library for Crystal like amazing Nokogiri

9 | 10 | I not pretend that **Crystagiri** does much as **Nokogiri**. All help will be welcome! :) 11 | 12 |
13 | 14 |

Developement

15 | 16 |
    17 |
  1. Clone this repository and go in it
  2. 18 |
  3. Generate the complete doc
  4. 19 |
  5. Run spec tests to ensure all work correctly
  6. 20 |
21 | 22 |

Contributing

23 | 24 |
    25 |
  1. Fork it
  2. 26 |
  3. Create your feature branch git checkout -b my-new-feature
  4. 27 |
  5. Commit your changes git commit -am "Add some feature"
  6. 28 |
  7. Push to the branch git push origin my-new-feature
  8. 29 |
  9. Create a new Pull Request
  10. 30 |
31 | 32 | 4 pers 33 | 34 |
35 | 36 | 37 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "spec" 2 | require "../src/crystagiri" 3 | -------------------------------------------------------------------------------- /src/crystagiri.cr: -------------------------------------------------------------------------------- 1 | require "./crystagiri/*" 2 | 3 | module Crystagiri 4 | # TODO Put your code here 5 | end 6 | -------------------------------------------------------------------------------- /src/crystagiri/html.cr: -------------------------------------------------------------------------------- 1 | require "http/client" 2 | require "xml" 3 | 4 | module Crystagiri 5 | # Represent an Html document who can be parsed 6 | class HTML 7 | getter :content 8 | getter :nodes 9 | 10 | # Initialize an Html object from Html source fetched 11 | # from the url 12 | def self.from_url(url : String, follow : Bool = false) : HTML 13 | begin 14 | response = HTTP::Client.get url 15 | if response.status_code == 200 16 | return HTML.new response.body 17 | elsif follow && response.status_code == 301 18 | from_url response.headers["Location"], follow: true 19 | else 20 | raise ArgumentError.new "Host returned #{response.status_code}" 21 | end 22 | rescue Socket::Error 23 | raise Socket::Error.new "Host #{url} cannot be fetched" 24 | end 25 | end 26 | 27 | # Initialize an Html object from content of file 28 | # designed by the given filepath 29 | def self.from_file(path : String) : HTML 30 | return HTML.new File.read(path) 31 | end 32 | 33 | # Transform the css query into an xpath query 34 | def self.css_query_to_xpath(query : String) : String 35 | query = "//#{query}" 36 | # Convert '#id_name' as '[@id="id_name"]' 37 | query = query.gsub /\#([A-z0-9]+-*_*)+/ { |m| "*[@id=\"%s\"]" % m.delete('#') } 38 | # Convert '.classname' as '[@class="classname"]' 39 | query = query.gsub /\.([A-z0-9]+-*_*)+/ { |m| "[@class=\"%s\"]" % m.delete('.') } 40 | # Convert ' > ' as '/' 41 | query = query.gsub /\s*>\s*/ { |m| "/" } 42 | # Convert ' ' as '//' 43 | query = query.gsub " ", "//" 44 | # a leading '*' when xpath does not include node name 45 | query = query.gsub /\/\[/ { |m| "/*[" } 46 | return query 47 | end 48 | 49 | # Initialize an Html object from Html source 50 | def initialize(@content : String) 51 | @nodes = XML.parse_html @content 52 | 53 | @ids = Hash(String, XML::Node).new 54 | @tags = Hash(String, Array(XML::Node)).new 55 | @classes = Hash(String, Array(XML::Node)).new 56 | 57 | visit @nodes # Build internal pointer map 58 | end 59 | 60 | # Functions used to populate internal maps 61 | 62 | private def add_id(id : String, node : XML::Node) 63 | @ids[id] = node 64 | end 65 | 66 | private def add_node(node : XML::Node) 67 | if @tags[node.name]? == nil 68 | @tags[node.name] = [] of XML::Node 69 | end 70 | @tags[node.name] << node 71 | end 72 | 73 | private def add_class(klass : String, node : XML::Node) 74 | if @classes[klass]? == nil 75 | @classes[klass] = [] of XML::Node 76 | end 77 | @classes[klass] << node 78 | end 79 | 80 | # Depth-first visit. Given a node, extract metadata from 81 | # node (if exists), then visit each child. 82 | private def visit(node : XML::Node) 83 | # We only extract metadata from HTML nodes 84 | if node.element? 85 | add_node node 86 | if to = node["id"]? 87 | add_id to, node 88 | end 89 | if classes = node["class"]? 90 | classes.split(' ') { |to| add_class to, node } 91 | end 92 | end 93 | # visit each child 94 | node.children.each do | child | 95 | visit child 96 | end 97 | end 98 | 99 | # Find first tag by tag name and return 100 | # `Crystagiri::Tag` founded or a nil if not founded 101 | def at_tag(tag_name : String) : Crystagiri::Tag | Nil 102 | if tags = @tags[tag_name]? 103 | tags.each do |tag| 104 | return Tag.new(tag).as Crystagiri::Tag 105 | end 106 | end 107 | return nil 108 | end 109 | 110 | # Find all nodes by tag name and yield 111 | # `Crystagiri::Tag` founded 112 | def where_tag(tag_name : String, &block) : Array(Tag) 113 | arr = [] of Crystagiri::Tag 114 | if tags = @tags[tag_name]? 115 | tags.each do |node| 116 | tag = Tag.new(node).as Crystagiri::Tag 117 | yield tag 118 | arr << tag 119 | end 120 | end 121 | return arr 122 | end 123 | 124 | # Find all nodes by classname and yield 125 | # `Crystagiri::Tag` founded 126 | def where_class(class_name : String, &block) : Array(Tag) 127 | arr = [] of Crystagiri::Tag 128 | if klasses = @classes[class_name]? 129 | klasses.each do |node| 130 | klass = Tag.new(node).as Crystagiri::Tag 131 | yield klass 132 | arr << klass 133 | end 134 | end 135 | return arr 136 | end 137 | 138 | # Find a node by its id and return a 139 | # `Crystagiri::Tag` founded or a nil if not founded 140 | def at_id(id_name : String) : Crystagiri::Tag | Nil 141 | if node = @ids[id_name]? 142 | return Tag.new(node).as Crystagiri::Tag 143 | end 144 | end 145 | 146 | # Find all node corresponding to the css query and yield 147 | # `Crystagiri::Tag` founded or a nil if not founded 148 | def css(query : String) : Array(Tag) 149 | query = HTML.css_query_to_xpath(query) 150 | return @nodes.xpath_nodes("//#{query}").map { |node| 151 | tag = Tag.new(node).as(Crystagiri::Tag) 152 | yield tag 153 | tag 154 | } 155 | end 156 | 157 | # Find first node corresponding to the css query and return 158 | # `Crystagiri::Tag` if founded or a nil if not founded 159 | def at_css(query : String) 160 | css(query) { |tag| return tag } 161 | return nil 162 | end 163 | end 164 | end 165 | -------------------------------------------------------------------------------- /src/crystagiri/tag.cr: -------------------------------------------------------------------------------- 1 | module Crystagiri 2 | class Tag 3 | getter :node 4 | 5 | def initialize(@node : XML::Node) 6 | end 7 | 8 | def classname : String | Nil 9 | return @node["class"]? ? @node["class"] : nil 10 | end 11 | 12 | def tagname : String 13 | return @node.name 14 | end 15 | 16 | def content : String 17 | return @node.text != nil ? @node.text.as(String) : "".as(String) 18 | end 19 | 20 | def parent : Tag | Nil 21 | if parent = @node.parent 22 | return Tag.new parent 23 | end 24 | nil 25 | end 26 | 27 | def children : Array(Tag) 28 | children = [] of Tag 29 | @node.children.each do |node| 30 | if node.element? 31 | children << Tag.new node 32 | end 33 | end 34 | children 35 | end 36 | 37 | def has_class?(klass : String) : Bool 38 | if classes = classname 39 | return classes.includes?(klass) 40 | end 41 | false 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /src/crystagiri/version.cr: -------------------------------------------------------------------------------- 1 | module Crystagiri 2 | VERSION = "0.4.0-alpha" 3 | end 4 | --------------------------------------------------------------------------------