diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..163eb75 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0bbd4a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +/docs/ +/lib/ +/bin/ +/.shards/ +*.dwarf + +# Libraries don't need dependency lock +# Dependencies will be locked in applications that use them +/shard.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..765f0e9 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: crystal + +# Uncomment the following if you'd like Travis to run specs and check code formatting +# script: +# - crystal spec +# - crystal tool format --check diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..21f35ce --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2019 Chris Watson + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..bea8e6f --- /dev/null +++ b/README.md @@ -0,0 +1,51 @@ +# opengraph.cr + +Crystal implementation of the [Open Graph](http://ogp.me) protocol, allowing you to parse Open Graph meta tags and extract valuable information. + +## Installation + +1. Add the dependency to your `shard.yml`: + + ```yaml + dependencies: + opengraph: + github: watzon/opengraph.cr + ``` + +2. Run `shards install` + +## Usage + +```crystal +require "opengraph" + +og = OpenGraph.from_url("css-tricks.com") +# og = OpenGraph.parse("") + +puts og.website? +# => true + +puts og["title"]? +# => "CSS-Tricks" + +puts og["image"]? +# => "https://css-tricks.com/wp-content/uploads/2014/03/css-tricks-star.png" +``` + +TODO: Write usage instructions here + +## Development + +TODO: Write development instructions here + +## Contributing + +1. Fork it () +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 + +## Contributors + +- [Chris Watson](https://github.com/watzon) - creator and maintainer diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..b47f350 --- /dev/null +++ b/shard.yml @@ -0,0 +1,13 @@ +name: opengraph +version: 0.1.0 + +authors: + - Chris Watson + +crystal: 0.28.0 + +license: MIT + +dependencies: + crystagiri: + github: madeindjs/crystagiri diff --git a/spec/opengraph_spec.cr b/spec/opengraph_spec.cr new file mode 100644 index 0000000..ae7e88f --- /dev/null +++ b/spec/opengraph_spec.cr @@ -0,0 +1,9 @@ +require "./spec_helper" + +describe Opengraph do + # TODO: Write tests + + it "works" do + false.should eq(true) + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..2917aa0 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/opengraph" diff --git a/src/opengraph.cr b/src/opengraph.cr new file mode 100644 index 0000000..aa147bc --- /dev/null +++ b/src/opengraph.cr @@ -0,0 +1,42 @@ +require "http/client" +require "crystagiri" +require "./opengraph/version" +require "./opengraph/object" + +# TODO: Write documentation for `Opengraph` +module OpenGraph + extend self + + # Fetch Open Graph data from the specified `url`. Makes an + # HTTP GET request and returns an `OpenGraph::Object` + # or nil if the URL was not found. + def self.from_url(url) + res = HTTP::Client.get(url) + + # Check for redirects + if (300..303).includes?(res.status_code) && res.headers["Location"]? + location = res.headers["Location"].to_s + return self.from_url(location) + end + + self.parse(res.body) + end + + # Parse raw `html` either from a string or an `XML::Node` + # and return an `OpenGraph::Object`. + def self.parse(html, strict = true) + doc = Crystagiri::HTML.new(html) + page = OpenGraph::Object.new + doc.css("meta") do |m| + attributes = m.node.attributes + if attributes["property"]? && attributes["property"].to_s.match(/["']og:(.+)['"]/i) + prop = $1.gsub("-", "_") + content = attributes["content"].to_s + if value = content.match(/["'](.+)['"]/i) + page[prop] = value[1].strip + end + end + end + page + end +end diff --git a/src/opengraph/object.cr b/src/opengraph/object.cr new file mode 100644 index 0000000..32e5e61 --- /dev/null +++ b/src/opengraph/object.cr @@ -0,0 +1,60 @@ +module OpenGraph + class Object < Hash(String, String) + MANDATORY_ATTRIBUTES = %w(title type image url) + + TYPES = { + "activity" => %w(activity sport), + "business" => %w(bar company cafe hotel restaurant), + "group" => %w(cause sports_league sports_team), + "organization" => %w(band government non_profit school university), + "person" => %w(actor athlete author director musician politician public_figure), + "place" => %w(city country landmark state_province), + "product" => %w(album book drink food game movie product song tv_show), + "website" => %w(blog website), + } + + def initialize + super + end + + def type + self["type"]? + end + + def schema + TYPES.each_pair do |schema, types| + return schema if types.includes?(self.type.to_s) + end + nil + end + + {% for type in TYPES.values.reduce { |acc, i| acc + i } %} + def {{type.id}}? + self.type == {{type}} + rescue + nil + end + + def {{type.id}} + self.type == {{type}} + end + {% end %} + + {% for scheme in TYPES.keys %} + def {{scheme.id}}? + self.type == {{scheme}} || TYPES[{{scheme}}].includes?(self.type) + end + + def {{scheme.id}} + self.type == {{scheme}} || TYPES[{{scheme}}].includes?(self.type) + rescue + nil + end + {% end %} + + def valid? + MANDATORY_ATTRIBUTES.each { |a| return false unless self[a]? } + true + end + end +end diff --git a/src/opengraph/version.cr b/src/opengraph/version.cr new file mode 100644 index 0000000..2d8e2eb --- /dev/null +++ b/src/opengraph/version.cr @@ -0,0 +1,3 @@ +module OpenGraph + VERSION = "0.1.0" +end