commit
0ceaffc7b0
|
@ -0,0 +1,29 @@
|
|||
name: website
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install Crystal
|
||||
uses: crystal-lang/install-crystal@v1
|
||||
with:
|
||||
crystal: nightly
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v2
|
||||
- name: Install dependencies
|
||||
run: shards install
|
||||
- name: Generate documentation
|
||||
run: crystal docs
|
||||
-
|
||||
name: Deploy to GitHub Pages
|
||||
if: success()
|
||||
uses: crazy-max/ghaction-github-pages@v2
|
||||
with:
|
||||
target_branch: gh-pages
|
||||
build_dir: docs
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -1,5 +1,6 @@
|
|||
# TODO: want to add methods with safe way like Ruby's refinement.
|
||||
# TODO: want to property's without open HTTP::Cookie class.
|
||||
|
||||
# This class represents http cookie.
|
||||
# open HTTP::Cookie class to add origin property.
|
||||
# origin property represents the origin of the resource.
|
||||
# if cookie's domain attribute isn't designated,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "./base_error"
|
||||
|
||||
# This error means matched elements are not found by *_with method.
|
||||
class Mechanize::ElementNotFoundError < Mechanize::Error
|
||||
getter element : Symbol
|
||||
getter conditions : String
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
require "http/client"
|
||||
|
||||
class Mechanize
|
||||
class File
|
||||
# property :body, :filename
|
||||
property :body, :code, uri, :response
|
||||
abstract class File
|
||||
# property :filename
|
||||
|
||||
# returns http response body
|
||||
getter body : String
|
||||
# returns http status code
|
||||
getter code : Int32
|
||||
# returns page uri
|
||||
getter uri : URI
|
||||
|
||||
def initialize(uri : URI, response : ::HTTP::Client::Response, body : String, code : Int32)
|
||||
@uri = uri
|
||||
|
|
|
@ -8,20 +8,31 @@ require "./form/button"
|
|||
require "./form/select_list"
|
||||
require "./utils/element_matcher"
|
||||
|
||||
# This class represents the form tag of html.
|
||||
class Mechanize::Form
|
||||
include Mechanize::ElementMatcher
|
||||
|
||||
getter node : Node | Lexbor::Node
|
||||
getter fields : Array(Mechanize::FormContent::Field)
|
||||
getter checkboxes : Array(Mechanize::FormContent::CheckBox)
|
||||
getter radiobuttons : Array(Mechanize::FormContent::RadioButton)
|
||||
getter selectboxes : Array(Mechanize::FormContent::MultiSelectList)
|
||||
getter buttons : Array(Mechanize::FormContent::Button)
|
||||
# returns hoge array of `Mechanize::FormContent::Field` in the form.
|
||||
getter fields : Array(FormContent::Field)
|
||||
# returns an array of input tags whose type is checkbox in the form.
|
||||
getter checkboxes : Array(FormContent::CheckBox)
|
||||
# returns an array of input tags whose type is radio in the form.
|
||||
getter radiobuttons : Array(FormContent::RadioButton)
|
||||
# returns an array of input tags whose type is select in the form.
|
||||
getter selectboxes : Array(FormContent::MultiSelectList)
|
||||
# returns an array of button tags and input tag whose type is button,submit,reset,image.
|
||||
getter buttons : Array(FormContent::Button)
|
||||
# returns form's 'enctype' attribute.
|
||||
getter enctype : String
|
||||
# returns form's 'method' attribute.
|
||||
getter method : String
|
||||
# returns form's 'name' attribute.
|
||||
getter name : String
|
||||
getter page : Mechanize::Page?
|
||||
# return form's 'action' attribute.
|
||||
property action : String
|
||||
# returns the page which includes the form.
|
||||
getter page : Mechanize::Page?
|
||||
|
||||
def initialize(node : Node | Lexbor::Node, page : Mechanize::Page? = nil)
|
||||
@enctype = node.fetch("enctype", "application/x-www-form-urlencoded")
|
||||
|
@ -57,7 +68,7 @@ class Mechanize::Form
|
|||
elements_with "checkbox", "checkboxes"
|
||||
elements_with "button"
|
||||
|
||||
# Returns all fields of type Textarea
|
||||
# Returns all fields of <input type="textarea">
|
||||
def textareas
|
||||
fields.select { |f| f.class == FormContent::Textarea }.map &.as(FormContent::Textarea)
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# This class represents button related html element.
|
||||
# <button>, and <input> whose type is button, reset, image, submit.
|
||||
class Mechanize::FormContent::Button < Mechanize::FormContent::Field
|
||||
getter form_node : Node | Lexbor::Node
|
||||
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
# This class represents <input type="checkbox">
|
||||
class Mechanize::FormContent::CheckBox < Mechanize::FormContent::RadioButton
|
||||
# set checkbox checked
|
||||
def check
|
||||
@checked = true
|
||||
end
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
# This class represents <input> elements in the form.
|
||||
class Mechanize::FormContent::Field
|
||||
# returns field's 'value' attribute
|
||||
property value : String?
|
||||
# returns field's 'name' attribute
|
||||
getter name : String
|
||||
# returns field's 'type' attribute
|
||||
getter type : String
|
||||
# returns field's 'value' attribute.
|
||||
# value property is changeable, but this property stores raw value.
|
||||
getter raw_value : String?
|
||||
getter node : Node | Lexbor::Node
|
||||
|
||||
|
@ -17,12 +23,12 @@ class Mechanize::FormContent::Field
|
|||
[@name, @value || ""]
|
||||
end
|
||||
|
||||
# returns DOM 'id' value
|
||||
# returns field's 'id' value
|
||||
def dom_id
|
||||
node.fetch("id", "")
|
||||
end
|
||||
|
||||
# returns DOM 'class' value
|
||||
# returns field's 'class' value
|
||||
def dom_class
|
||||
node.fetch("class", "")
|
||||
end
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="hidden">
|
||||
class Mechanize::FormContent::Hidden < Mechanize::FormContent::Field
|
||||
end
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="image">
|
||||
class Mechanize::FormContent::ImageButton < Mechanize::FormContent::Button
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "./option"
|
||||
|
||||
# This class represents <select multiple>
|
||||
class Mechanize::FormContent::MultiSelectList
|
||||
getter node : Lexbor::Node
|
||||
getter name : String
|
||||
|
@ -18,32 +19,37 @@ class Mechanize::FormContent::MultiSelectList
|
|||
}
|
||||
end
|
||||
|
||||
# set all options unchecked
|
||||
def select_none
|
||||
@values = Array(String).new
|
||||
options.each &.unselect
|
||||
end
|
||||
|
||||
# set all options checked
|
||||
def select_all
|
||||
@values = Array(String).new
|
||||
options.each &.select
|
||||
end
|
||||
|
||||
# returns all checked options
|
||||
def selected_options
|
||||
options.select &.selected?
|
||||
end
|
||||
|
||||
# add new values to options
|
||||
def values=(new_values)
|
||||
select_none
|
||||
new_values.each do |value|
|
||||
option = options.find { |o| o.value == value }
|
||||
if option.nil?
|
||||
@value.push(value)
|
||||
@values.push(value)
|
||||
else
|
||||
option.select
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# return all option's values.
|
||||
def values
|
||||
@values + selected_options.map &.value
|
||||
end
|
||||
|
|
|
@ -30,6 +30,7 @@ class Mechanize::FormContent::Option
|
|||
@selected = !@selected
|
||||
end
|
||||
|
||||
# returns option checked or not
|
||||
def selected?
|
||||
@selected
|
||||
end
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
# This class represents <input type="radio">
|
||||
class Mechanize::FormContent::RadioButton < Mechanize::FormContent::Field
|
||||
property :checked, :form
|
||||
|
||||
|
@ -7,19 +8,23 @@ class Mechanize::FormContent::RadioButton < Mechanize::FormContent::Field
|
|||
super(node)
|
||||
end
|
||||
|
||||
# set radiobutton checked
|
||||
def check
|
||||
uncheck_peers
|
||||
@checked = true
|
||||
end
|
||||
|
||||
# set radiobutton checked
|
||||
def uncheck
|
||||
@checked = false
|
||||
end
|
||||
|
||||
# change radiobutton state checked or unchecked
|
||||
def click
|
||||
checked ? uncheck : check
|
||||
end
|
||||
|
||||
# returns radiobutton checked or not
|
||||
def checked?
|
||||
checked
|
||||
end
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="reset">
|
||||
class Mechanize::FormContent::ResetButton < Mechanize::FormContent::Button
|
||||
end
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
require "./multi_select_list"
|
||||
|
||||
# This class represents <select> which is not multiple
|
||||
class Mechanize::FormContent::SelectList < Mechanize::FormContent::MultiSelectList
|
||||
def initialize(node)
|
||||
super node
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="submit">
|
||||
class Mechanize::FormContent::SubmitButton < Mechanize::FormContent::Button
|
||||
end
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="text">
|
||||
class Mechanize::FormContent::Text < Mechanize::FormContent::Field
|
||||
end
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
# This class represents <input type="textarea">
|
||||
class Mechanize::FormContent::Textarea < Mechanize::FormContent::Field
|
||||
end
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
require "./page"
|
||||
|
||||
# This class represents the history of http response you sent.
|
||||
# If you send a request, mechanize saves the history.
|
||||
class Mechanize
|
||||
class History
|
||||
# max page size history can save. default is 100.
|
||||
# as same as `agent.max_history`.
|
||||
property max_size : Int32
|
||||
property array : Array(Mechanize::Page)
|
||||
|
||||
|
@ -12,6 +16,7 @@ class Mechanize
|
|||
@array = array
|
||||
end
|
||||
|
||||
# add page to history.
|
||||
def push(page, uri = nil)
|
||||
@array.push(page)
|
||||
while size > @max_size
|
||||
|
@ -20,6 +25,7 @@ class Mechanize
|
|||
self
|
||||
end
|
||||
|
||||
# take the last page out from history.
|
||||
def pop
|
||||
if size == 0
|
||||
# TODO: raise error
|
||||
|
|
|
@ -6,12 +6,13 @@ require "../history"
|
|||
class Mechanize
|
||||
module HTTP
|
||||
class Agent
|
||||
property :request_headers, :context
|
||||
property request_headers : ::HTTP::Headers
|
||||
property context : Mechanize?
|
||||
property history : Mechanize::History
|
||||
property user_agent : String
|
||||
property request_cookies : ::HTTP::Cookies
|
||||
|
||||
def initialize(@context : Mechanize | Nil = nil)
|
||||
def initialize(@context : Mechanize? = nil)
|
||||
@history = Mechanize::History.new
|
||||
@request_headers = ::HTTP::Headers.new
|
||||
@context = context
|
||||
|
@ -19,6 +20,9 @@ class Mechanize
|
|||
@user_agent = ""
|
||||
end
|
||||
|
||||
# send http request and return page.
|
||||
# This method is called from Mechanize#get, #post and other methods.
|
||||
# There's no need to call this method directly.
|
||||
def fetch(uri, method = :get, headers = ::HTTP::Headers.new, params = Hash(String, String).new,
|
||||
referer = (current_page unless history.empty?))
|
||||
uri = resolve_url(uri, referer)
|
||||
|
@ -51,7 +55,8 @@ class Mechanize
|
|||
fetch(uri)
|
||||
end
|
||||
|
||||
def http_request(uri, method, params)
|
||||
# send http request
|
||||
private def http_request(uri, method, params) : ::HTTP::Client::Response?
|
||||
case uri.scheme.not_nil!.downcase
|
||||
when "http", "https"
|
||||
case method
|
||||
|
@ -63,20 +68,34 @@ class Mechanize
|
|||
end
|
||||
end
|
||||
|
||||
def current_page
|
||||
# returns the page now mechanize visiting.
|
||||
# ```
|
||||
# agent.current_page
|
||||
# ```
|
||||
def current_page : Mechanize::Page
|
||||
@history.last
|
||||
end
|
||||
|
||||
def back
|
||||
# returns the page mechanize previous visited.
|
||||
# ```
|
||||
# agent.back
|
||||
# ```
|
||||
def back : Mechanize::Page
|
||||
@history.pop
|
||||
end
|
||||
|
||||
# Get maximum number of items allowed in the history. The default setting is 100 pages.
|
||||
def max_history
|
||||
# ```
|
||||
# agent.max_history # => 100
|
||||
# ```
|
||||
def max_history : Int32
|
||||
@history.max_size
|
||||
end
|
||||
|
||||
# Set maximum number of items allowed in the history.
|
||||
# ```
|
||||
# agent.max_history = 1000
|
||||
# ```
|
||||
def max_history=(length)
|
||||
@history.max_size = length
|
||||
end
|
||||
|
|
|
@ -2,13 +2,14 @@ require "./file"
|
|||
require "./utils/element_matcher"
|
||||
require "./page/link"
|
||||
|
||||
# This class represents the page of response.
|
||||
# If you send request, it returns the instance of Page.
|
||||
# You can get status code, title, and page body, and search html node using css selector.
|
||||
|
||||
# This class represents the result of http response.
|
||||
# If you send a request, it returns the instance of `Mechanize::Page`.
|
||||
# You can get status code, title, and page body, and search html node using css selector from page instance.
|
||||
class Mechanize
|
||||
class Page < Mechanize::File
|
||||
include Mechanize::ElementMatcher
|
||||
|
||||
# look at lexbor document.(https://github.com/kostya/lexbor#readme)
|
||||
delegate :css, to: parser
|
||||
|
||||
property mech : Mechanize
|
||||
|
@ -25,6 +26,9 @@ class Mechanize
|
|||
end
|
||||
|
||||
# return page title.
|
||||
# ```
|
||||
# page.title # => String
|
||||
# ```
|
||||
def title : String
|
||||
title_node = css("title")
|
||||
if title_node.empty?
|
||||
|
@ -35,19 +39,25 @@ class Mechanize
|
|||
end
|
||||
|
||||
# return all forms(`Mechanize::Form`) in the page.
|
||||
# ```
|
||||
# page.forms # => Array(Mechanize::Form)
|
||||
# ```
|
||||
def forms : Array(Mechanize::Form)
|
||||
forms = css("form").map do |html_form|
|
||||
form = Mechanize::Form.new(html_form, self)
|
||||
form = Form.new(html_form, self)
|
||||
form.action ||= @uri.to_s
|
||||
form
|
||||
end.to_a
|
||||
end
|
||||
|
||||
# return all links(`Mechanize::PageContent::Link) in the page.
|
||||
# return all links(`Mechanize::PageContent::Link`) in the page.
|
||||
# ```
|
||||
# page.links # => Array(Mechanize::PageContent::Link)
|
||||
# ```
|
||||
def links : Array(Mechanize::PageContent::Link)
|
||||
links = %w{a area}.map do |tag|
|
||||
css(tag).map do |node|
|
||||
Mechanize::PageContent::Link.new(node, @mech, self)
|
||||
PageContent::Link.new(node, @mech, self)
|
||||
end
|
||||
end.flatten
|
||||
end
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
# This class represents link element like <a> and <area>.
|
||||
# The instance of this class is clickable.
|
||||
class Mechanize::PageContent::Link
|
||||
getter node : Lexbor::Node
|
||||
getter page : Mechanize::Page
|
||||
|
|
|
@ -1,55 +1,73 @@
|
|||
class Mechanize
|
||||
# This module is for macros making *_with methods.
|
||||
# These methods are useful for searching elements by its' attribute.
|
||||
module ElementMatcher
|
||||
macro elements_with(singular, plural = "")
|
||||
{% plural = "#{singular.id}s" if plural.empty? %}
|
||||
# search {{ singular.id }} which matches condition.
|
||||
#
|
||||
# Examples
|
||||
# ```
|
||||
# # if you specify String like "foo", it searches form which name is "foo".
|
||||
# # like {<form name="foo"></form>}
|
||||
# page.form_with("foo")
|
||||
#
|
||||
# # you can specify tag's attribute and its' value by NamedTuple or Hash(String, String).
|
||||
# ex) <form class="foo"></form>
|
||||
# page.form_with("class" => "foo")
|
||||
# page.form_with(class: "foo")
|
||||
# ```
|
||||
def {{plural.id}}_with(criteria : String | NamedTuple | Hash(String,String))
|
||||
{{plural.id}}_with(criteria){}
|
||||
end
|
||||
{% plural = "#{singular.id}s" if plural.empty? %}
|
||||
# search {{ plural.id }} which match condition.
|
||||
#
|
||||
# Examples
|
||||
# ```
|
||||
# # if you specify String like "foo", it searches {{ singular.id }} which name is "foo".
|
||||
{% if ["form", "button"].includes?("#{singular.id}") %}
|
||||
# # like <{{ singular.id }} name="foo"></{{ singular.id }}>
|
||||
{% elsif "#{singular.id}" == "field" %}
|
||||
# # like <input name="foo"></input>
|
||||
{% elsif "#{singular.id}" == "radiobutton" %}
|
||||
# # like <input type="radio" name="foo"></input>
|
||||
{% else %}
|
||||
# # like <input type="{{ singular.id }}" name="foo"></input>
|
||||
{% end %}
|
||||
# page.{{ plural.id }}_with("foo")
|
||||
# # you can also specify tag's attribute and its' value by NamedTuple or Hash(String, String).
|
||||
{% if ["form", "button"].includes?("#{singular.id}") %}
|
||||
# # ex) <{{ singular.id }} class="foo"></{{ singular.id }}>
|
||||
{% elsif "#{singular.id}" == "field" %}
|
||||
# # ex) <input class="foo"></input>
|
||||
{% elsif "#{singular.id}" == "radiobutton" %}
|
||||
# # ex) <input type="radio" class="foo"></input>
|
||||
{% else %}
|
||||
# # ex) <input type="{{ singular.id }}" class="foo"></input>
|
||||
{% end %}
|
||||
# page.{{ plural.id }}_with("class" => "foo")
|
||||
# page.{{ plural.id }}_with(class: "foo")
|
||||
# ```
|
||||
def {{plural.id}}_with(criteria : String | NamedTuple | Hash(String,String))
|
||||
{{plural.id}}_with(criteria){}
|
||||
end
|
||||
|
||||
def {{plural.id}}_with(criteria, &block)
|
||||
if criteria.is_a?(NamedTuple)
|
||||
criteria = criteria.to_h
|
||||
end
|
||||
if criteria.is_a?(String)
|
||||
criteria = {"name" => criteria}
|
||||
else
|
||||
criteria = criteria.each_with_object(Hash(String,String).new) do |(k, v), h|
|
||||
k = k.to_s
|
||||
h[k] = v
|
||||
def {{plural.id}}_with(criteria, &block)
|
||||
if criteria.is_a?(NamedTuple)
|
||||
criteria = criteria.to_h
|
||||
end
|
||||
end
|
||||
f = {{plural.id}}.select do |elm|
|
||||
criteria.all? do |k,v|
|
||||
if k == "text"
|
||||
v == elm.node.inner_text
|
||||
else
|
||||
v == elm.node.fetch(k,"")
|
||||
if criteria.is_a?(String)
|
||||
criteria = {"name" => criteria}
|
||||
else
|
||||
criteria = criteria.each_with_object(Hash(String,String).new) do |(k, v), h|
|
||||
k = k.to_s
|
||||
h[k] = v
|
||||
end
|
||||
end
|
||||
f = {{plural.id}}.select do |elm|
|
||||
criteria.all? do |k,v|
|
||||
if k == "text"
|
||||
v == elm.node.inner_text
|
||||
else
|
||||
v == elm.node.fetch(k,"")
|
||||
end
|
||||
end
|
||||
end
|
||||
yield f
|
||||
f
|
||||
end
|
||||
yield f
|
||||
f
|
||||
end
|
||||
|
||||
def {{singular.id}}_with(criteria)
|
||||
f = {{plural.id}}_with(criteria)
|
||||
# TODO: Write correct error message.
|
||||
raise Mechanize::ElementNotFoundError.new(:{{singular.id}}, "") if f.empty?
|
||||
f.first
|
||||
# returns first element of `#{{ plural.id }}_with`
|
||||
def {{singular.id}}_with(criteria)
|
||||
f = {{plural.id}}_with(criteria)
|
||||
# TODO: Write correct error message.
|
||||
raise Mechanize::ElementNotFoundError.new(:{{singular.id}}, "") if f.empty?
|
||||
f.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue