content-preview 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/.rspec +2 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +39 -0
- data/MIT-LICENSE +20 -0
- data/README.md +13 -0
- data/Rakefile +16 -0
- data/bin/cp-server +46 -0
- data/config.ru +23 -0
- data/content-preview.gemspec +28 -0
- data/js/src/content-preview.coffee +100 -0
- data/lib/content-preview.rb +4 -0
- data/lib/content-preview/parser.rb +77 -0
- data/lib/content-preview/router.rb +31 -0
- data/lib/content-preview/router/handlers/default.rb +22 -0
- data/lib/content-preview/router/router.rb +25 -0
- data/lib/content-preview/router/routes.yml +2 -0
- data/lib/content-preview/version.rb +3 -0
- data/lib/tasks/content-preview_tasks.rake +4 -0
- data/roadmap.txt +90 -0
- data/spec/parser/parser_spec.rb +65 -0
- data/spec/spec_helper.rb +17 -0
- metadata +170 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
content-preview (0.0.1)
|
5
|
+
json
|
6
|
+
nokogiri
|
7
|
+
rack-cors
|
8
|
+
rack-test
|
9
|
+
rake
|
10
|
+
thor
|
11
|
+
|
12
|
+
GEM
|
13
|
+
remote: http://rubygems.org/
|
14
|
+
specs:
|
15
|
+
diff-lcs (1.1.3)
|
16
|
+
json (1.7.5)
|
17
|
+
nokogiri (1.5.5)
|
18
|
+
rack (1.4.1)
|
19
|
+
rack-cors (0.2.7)
|
20
|
+
rack
|
21
|
+
rack-test (0.6.1)
|
22
|
+
rack (>= 1.0)
|
23
|
+
rake (0.9.2.2)
|
24
|
+
rspec (2.11.0)
|
25
|
+
rspec-core (~> 2.11.0)
|
26
|
+
rspec-expectations (~> 2.11.0)
|
27
|
+
rspec-mocks (~> 2.11.0)
|
28
|
+
rspec-core (2.11.1)
|
29
|
+
rspec-expectations (2.11.2)
|
30
|
+
diff-lcs (~> 1.1.3)
|
31
|
+
rspec-mocks (2.11.2)
|
32
|
+
thor (0.16.0)
|
33
|
+
|
34
|
+
PLATFORMS
|
35
|
+
ruby
|
36
|
+
|
37
|
+
DEPENDENCIES
|
38
|
+
content-preview!
|
39
|
+
rspec
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
# Content Preview
|
2
|
+
|
3
|
+
This is a simple service for extracting media content from user posted text.
|
4
|
+
It can be used on any chunk of text to get the contained links or HTML and get the relevant media informations.
|
5
|
+
It'll behave like FB's content preview when you paste a link inside a message's text field.
|
6
|
+
|
7
|
+
## Development
|
8
|
+
|
9
|
+
Currently under development ...
|
10
|
+
|
11
|
+
## Licence
|
12
|
+
|
13
|
+
This software is licenced under the MIT licence
|
data/Rakefile
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'bundler/setup'
|
6
|
+
rescue LoadError
|
7
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
8
|
+
end
|
9
|
+
|
10
|
+
Bundler::GemHelper.install_tasks
|
11
|
+
|
12
|
+
|
13
|
+
RSpec::Core::RakeTask.new('spec')
|
14
|
+
task :default => :spec
|
15
|
+
|
16
|
+
|
data/bin/cp-server
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'thor'
|
4
|
+
require 'json'
|
5
|
+
require 'rack'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
require 'content-preview/router'
|
9
|
+
require 'content-preview'
|
10
|
+
|
11
|
+
require 'rack'
|
12
|
+
require 'rack/cors'
|
13
|
+
|
14
|
+
class ContentPreviewServer < Thor
|
15
|
+
|
16
|
+
desc "start", "Starts a Rack server with the Rack::Handler and Port given as argument Defaults are Webrick and"
|
17
|
+
method_option :server, aliases: '-s', default: 'WEBrick', desc: "The Rack::Handler server to use (Webrick, Mongrel, Thin ...)"
|
18
|
+
method_option :port, aliases: '-p', default: '3210', desc: "The port to run the server on"
|
19
|
+
method_option :origin, aliases: '-o', default: '*', desc: "Allowed client origins"
|
20
|
+
def start
|
21
|
+
origin, port, server = options[:origin], options[:port], options[:server]
|
22
|
+
|
23
|
+
app = Rack::Builder.new do
|
24
|
+
use Rack::CommonLogger
|
25
|
+
use Rack::ShowExceptions
|
26
|
+
use Rack::Lint
|
27
|
+
|
28
|
+
use Rack::Cors do
|
29
|
+
allow do
|
30
|
+
origins origin
|
31
|
+
resource %r{/},
|
32
|
+
:headers => ['Origin', 'Accept', 'Content-Type'],
|
33
|
+
:methods => [:get, :post]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
puts "Launch handler #{ server } on port #{ port }"
|
38
|
+
run ContentPreview::Router::Base
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
Rack::Handler.const_get(server).run(app, Host: '0.0.0.0', Port: port)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
ContentPreviewServer.start
|
data/config.ru
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'rack'
|
3
|
+
require 'yaml'
|
4
|
+
require 'rack/cors'
|
5
|
+
|
6
|
+
require 'content-preview/router'
|
7
|
+
require 'content-preview'
|
8
|
+
|
9
|
+
use ::Rack::CommonLogger
|
10
|
+
# use ::Rack::ShowExceptions
|
11
|
+
use ::Rack::Lint
|
12
|
+
#use Rack::Static, :urls => ["/static"]
|
13
|
+
|
14
|
+
use Rack::Cors do
|
15
|
+
allow do
|
16
|
+
origins 'http://localhost:3000'
|
17
|
+
resource %r{/},
|
18
|
+
:headers => ['Origin', 'Accept', 'Content-Type'],
|
19
|
+
:methods => [:get, :post]
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
run ContentPreview::Router::Base.new
|
@@ -0,0 +1,28 @@
|
|
1
|
+
$:.push File.expand_path("../lib", __FILE__)
|
2
|
+
|
3
|
+
# Maintain your gem's version:
|
4
|
+
require "content-preview/version"
|
5
|
+
|
6
|
+
# Describe your gem and declare its dependencies:
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "content-preview"
|
9
|
+
s.version = ContentPreview::VERSION
|
10
|
+
s.authors = ["Glyph"]
|
11
|
+
s.email = ["vala@glyph.fr"]
|
12
|
+
s.homepage = "http://glyph.fr"
|
13
|
+
s.summary = "A simple service for getting URL page informations"
|
14
|
+
s.description = "A simple service for getting URL page informations"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.test_files = `git ls-files -- {spec}/*`.split("\n")
|
19
|
+
s.bindir = "bin"
|
20
|
+
s.executables = 'cp-server'
|
21
|
+
|
22
|
+
s.add_dependency 'thor'
|
23
|
+
s.add_dependency 'nokogiri'
|
24
|
+
s.add_dependency 'rake'
|
25
|
+
s.add_dependency 'rack-cors'
|
26
|
+
s.add_dependency 'rack-test'
|
27
|
+
s.add_dependency 'json'
|
28
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
class ContentPreview
|
2
|
+
constructor: () ->
|
3
|
+
@state = null
|
4
|
+
|
5
|
+
process: (str, callback) ->
|
6
|
+
url = @parseStr(str)
|
7
|
+
# Only request informations if we found an url
|
8
|
+
unless !url || url == @state
|
9
|
+
# Keep state for future preview requests
|
10
|
+
@state = url
|
11
|
+
@requestPreview(url, callback)
|
12
|
+
|
13
|
+
# Parsing
|
14
|
+
# Checking if str matches url regex
|
15
|
+
parseStr: (str) ->
|
16
|
+
urls = str.match(/(http\:\/\/[^\s]+)/)
|
17
|
+
# Return first occurence
|
18
|
+
urls[1] if urls
|
19
|
+
|
20
|
+
# Send request to remote service
|
21
|
+
requestPreview: (url, callback) ->
|
22
|
+
$.get $.fn.parseContentPreview.defaultOptions.url, { url: url }, ((resp) => @processResponse(resp, callback)), 'json'
|
23
|
+
|
24
|
+
# Response handling
|
25
|
+
processResponse: (resp, callback) ->
|
26
|
+
#callback(resp) if resp
|
27
|
+
|
28
|
+
# setting preview_* values to empty
|
29
|
+
$($.fn.parseContentPreview.defaultOptions.preview_title).text("")
|
30
|
+
$($.fn.parseContentPreview.defaultOptions.preview_description).text("")
|
31
|
+
$($.fn.parseContentPreview.defaultOptions.preview_image).empty()
|
32
|
+
$(".images").empty()
|
33
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).empty()
|
34
|
+
|
35
|
+
if resp.title
|
36
|
+
$($.fn.parseContentPreview.defaultOptions.preview_title).text(resp.title)
|
37
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).prepend('<input type="hidden" name="' + $.fn.parseContentPreview.defaultOptions.form_content_name + '[content_title]" value="' + resp.title + '"/>')
|
38
|
+
|
39
|
+
if resp.description
|
40
|
+
$($.fn.parseContentPreview.defaultOptions.preview_description).text(resp.description)
|
41
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).prepend('<input type="hidden" name="' + $.fn.parseContentPreview.defaultOptions.form_content_name + '[content_description]" value="' + resp.description + '"/>')
|
42
|
+
|
43
|
+
if resp.image
|
44
|
+
$($.fn.parseContentPreview.defaultOptions.preview_image).prepend('<img width="200px" src="' + resp.image + '" />')
|
45
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).prepend('<input type="hidden" name="' + $.fn.parseContentPreview.defaultOptions.form_content_name + '[content_image]" value="' + resp.image + '"/>')
|
46
|
+
|
47
|
+
if resp.images
|
48
|
+
resp.images.map (item) ->
|
49
|
+
unless (item.substr(0, 7) is "http://") || (item.substr(0, 2) is "//")
|
50
|
+
item = textarea.val() + item
|
51
|
+
$(".images").prepend('<img id="image_remote_preview_" src="' + item + '" />')
|
52
|
+
|
53
|
+
# show cross image if one the meta data is available
|
54
|
+
if resp.title or resp.description or resp.image or resp.images
|
55
|
+
$("#form-actions-cross").show()
|
56
|
+
|
57
|
+
$.fn.parseContentPreview = (str, callback) ->
|
58
|
+
this.each ->
|
59
|
+
# Get preview object or build a new if none exist
|
60
|
+
# preview = $(this).data('content-preview')
|
61
|
+
|
62
|
+
preview = new ContentPreview()
|
63
|
+
# Process str
|
64
|
+
preview.process str, callback
|
65
|
+
|
66
|
+
# default options
|
67
|
+
$.fn.parseContentPreview.defaultOptions = {
|
68
|
+
url: 'http://def.rs.af.cm',
|
69
|
+
form_content_name: 'post',
|
70
|
+
form_hidden_inputs_name: '.hidden_inputs',
|
71
|
+
preview_title: '#preview-title',
|
72
|
+
preview_description: '#preview-description',
|
73
|
+
preview_image: '#preview-image'
|
74
|
+
};
|
75
|
+
|
76
|
+
# method to select image from many images and prepend it to the form_hidden_inputs_name
|
77
|
+
preview_select_image = (image) ->
|
78
|
+
if $("input[name='" + $.fn.parseContentPreview.defaultOptions.form_content_name + "[content_image]']").length > 0
|
79
|
+
$("input[name='" + $.fn.parseContentPreview.defaultOptions.form_content_name + "[content_image]']").attr "value", image
|
80
|
+
else
|
81
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).prepend "<input type='hidden' name='" + $.fn.parseContentPreview.defaultOptions.form_content_name + "[content_image]' value='" + image + "'/>"
|
82
|
+
|
83
|
+
$ ->
|
84
|
+
$("img[id='image_remote_preview_']").click (e) ->
|
85
|
+
preview_select_image $(e.currentTarget).attr('src')
|
86
|
+
return
|
87
|
+
|
88
|
+
$("#form-actions-cross").click (e) ->
|
89
|
+
$($.fn.parseContentPreview.defaultOptions.preview_title).text("")
|
90
|
+
$($.fn.parseContentPreview.defaultOptions.preview_description).text("")
|
91
|
+
$($.fn.parseContentPreview.defaultOptions.preview_image).empty()
|
92
|
+
$(".images").empty()
|
93
|
+
$($.fn.parseContentPreview.defaultOptions.form_hidden_inputs_name).empty()
|
94
|
+
|
95
|
+
$("#form-actions-cross").hide()
|
96
|
+
return
|
97
|
+
|
98
|
+
# Expose
|
99
|
+
window.ContentPreview = ContentPreview
|
100
|
+
window.preview_select_image = preview_select_image
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'nokogiri'
|
3
|
+
|
4
|
+
module ContentPreview
|
5
|
+
class Parser
|
6
|
+
attr_accessor :title, :description, :images
|
7
|
+
|
8
|
+
def initialize(images = [])
|
9
|
+
self.images = images
|
10
|
+
end
|
11
|
+
|
12
|
+
def process(url)
|
13
|
+
return unless url =~ /^http\:\/\//
|
14
|
+
|
15
|
+
begin
|
16
|
+
result = {}
|
17
|
+
document = Nokogiri::HTML(open(url))
|
18
|
+
process_open_graph(document)
|
19
|
+
process_meta_data(document, url)
|
20
|
+
|
21
|
+
result['title'] = self.title
|
22
|
+
result['description'] = self.description
|
23
|
+
result['images'] = self.images
|
24
|
+
|
25
|
+
return result
|
26
|
+
rescue Exception => e
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def process_open_graph(document)
|
32
|
+
unless document.xpath('//meta[starts-with(@property, "og:")]').empty?
|
33
|
+
for tag in document.xpath('//meta[starts-with(@property, "og:")]') do
|
34
|
+
case tag.first.last
|
35
|
+
when 'og:title'
|
36
|
+
self.title = tag['content']
|
37
|
+
|
38
|
+
when 'og:description'
|
39
|
+
self.description = tag['content']
|
40
|
+
|
41
|
+
when 'og:image'
|
42
|
+
self.images << tag['content']
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def process_meta_data(document, url)
|
49
|
+
unless self.title
|
50
|
+
unless document.css('title').empty?
|
51
|
+
self.title = document.css('title').text
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
if self.images.empty?
|
56
|
+
list = []
|
57
|
+
document.traverse do |el|
|
58
|
+
[el[:src], el[:href]].grep(/\.(jpg)$/i).map{|l| URI.join(url, l).to_s}.first(10).each do |image|
|
59
|
+
list << image
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
self.images = list
|
64
|
+
end
|
65
|
+
|
66
|
+
unless self.description
|
67
|
+
unless document.xpath('//meta[starts-with(@name, "")]').empty?
|
68
|
+
for tag in document.xpath('//meta[starts-with(@name, "")]') do
|
69
|
+
if %w(description).include?(tag.first.last)
|
70
|
+
self.description = tag['content']
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'content-preview/router/handlers/default'
|
2
|
+
|
3
|
+
module ContentPreview
|
4
|
+
module Router
|
5
|
+
class Base
|
6
|
+
def initialize(path = File.expand_path('../router/routes.yml', __FILE__))
|
7
|
+
@routes = YAML.load_file path
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
path = env['PATH_INFO']
|
12
|
+
|
13
|
+
until @routes.has_key? path do
|
14
|
+
path = path.rpartition('/').first
|
15
|
+
path = '/' if path.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
# require File.dirname(__FILE__) + '/' + @routes[path]
|
19
|
+
|
20
|
+
class_name = @routes[path].capitalize
|
21
|
+
Handlers.const_get(class_name).call env
|
22
|
+
end
|
23
|
+
|
24
|
+
class << self
|
25
|
+
def call *args
|
26
|
+
new.call *args
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module ContentPreview
|
2
|
+
module Router
|
3
|
+
module Handlers
|
4
|
+
class Default
|
5
|
+
def self.call(env)
|
6
|
+
req = Rack::Request.new(env)
|
7
|
+
if req.get?
|
8
|
+
[404, {'Content-Type' => 'text/html'}, StringIO.new('No content param found.')] if !req.params['url']
|
9
|
+
result = ContentPreview::Parser.new.process(req.params["url"])
|
10
|
+
if result
|
11
|
+
[200, {'Content-Type' => 'text/json'}, StringIO.new(result.to_json)]
|
12
|
+
else
|
13
|
+
[404, {'Content-Type' => 'text/html'}, StringIO.new('The content you asked could not be found.')]
|
14
|
+
end
|
15
|
+
else
|
16
|
+
[403, {'Content-Type' => 'text/html'}, StringIO.new('Unauthorized.')]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'content-preview/server/handlers/default'
|
2
|
+
|
3
|
+
module ContentPreview
|
4
|
+
module Router
|
5
|
+
class Base
|
6
|
+
def initialize(path = File.expand_path('../routes.yml', __FILE__))
|
7
|
+
@routes = YAML.load_file path
|
8
|
+
end
|
9
|
+
|
10
|
+
def call(env)
|
11
|
+
path = env['PATH_INFO']
|
12
|
+
|
13
|
+
until @routes.has_key? path do
|
14
|
+
path = path.rpartition('/').first
|
15
|
+
path = '/' if path.empty?
|
16
|
+
end
|
17
|
+
|
18
|
+
require File.dirname(__FILE__) + '/' + @routes[path]
|
19
|
+
|
20
|
+
class_name = @routes[path].capitalize
|
21
|
+
Handlers.const_get(class_name).call env
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/roadmap.txt
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
ROADMAP :
|
2
|
+
|
3
|
+
----------------------------------------------------------------------
|
4
|
+
|
5
|
+
V.0.1 :
|
6
|
+
|
7
|
+
Développement du parser et de différentes stratégies permettant un traitement plus approfondi et spécifique pour certaines URLS connues.
|
8
|
+
|
9
|
+
Description très basique :
|
10
|
+
|
11
|
+
1. Le parser récupère une URL en entrée
|
12
|
+
2. Le parser d'URL s'occupe de récupérer la page HTML correspondant à l'URL
|
13
|
+
3. Les headers checkés en priorité sont :
|
14
|
+
a. Les balise meta OpenGraph (og:xxx)
|
15
|
+
b. <title>, <meta description>
|
16
|
+
c. Les images contenues dans la page
|
17
|
+
4. Le parser renvoie soit une réponse vide si rien n'est trouvé et sinon un hash de type :
|
18
|
+
{
|
19
|
+
title: "Super video",
|
20
|
+
description: "Excellent one, gotta love it",
|
21
|
+
type: "video",
|
22
|
+
images: [
|
23
|
+
"http://example.com/image1.jpg",
|
24
|
+
"http://example.com/image2.jpg",
|
25
|
+
"http://example.com/image3.jpg"
|
26
|
+
]
|
27
|
+
}
|
28
|
+
|
29
|
+
----------------------------------------------------------------------
|
30
|
+
|
31
|
+
V.0.2 :
|
32
|
+
|
33
|
+
Il faut transformer le composant en service HTTP très simple, recevant des requêtes POST ayant pour unique paramètre le champ contenant le texte de l'utilisateur à parser.
|
34
|
+
|
35
|
+
1. Le service reçoit une réponse POST contenant un champ "content" contenant le texte à parser
|
36
|
+
2. Il appelle ContentPreview#parse avec le champ "content" comme argument afin de déléguer la tâche au parser
|
37
|
+
3. Selon la réponse successful ou non du parser, un code HTTP différent est renvoyé ainsi que le résultat du parsing ou non :
|
38
|
+
a. Le parser renvoie une réponse vide (nil, s'évaluant à false), indiquant que rien n'a été trouvé, le service renvoie un code 404
|
39
|
+
b. Le parser renvoie un hash (s'évaluant à true), celui-ci est converti en JSON et renvoyé avec un code 200
|
40
|
+
|
41
|
+
|
42
|
+
----------------------------------------------------------------------
|
43
|
+
|
44
|
+
V.0.3 :
|
45
|
+
|
46
|
+
Développer un client JS (dépendant de jQuery) permettant de parser un contenu texte et/ou HTML, d'interroger le serveur sur d'éventuels contenus media et d'appeler un callback défini par l'utisateur si un contenu media est renvoyé.
|
47
|
+
Il s'accompagnera d'un plugin jQuery permettant l'utilisation de $.fn.data() afin de persister l'état de la preview et ne la rafraîchir qu'en cas de besoin.
|
48
|
+
|
49
|
+
Quelques spécifications :
|
50
|
+
|
51
|
+
1. La méthode #parse est appelée sur le client et prend en argument :
|
52
|
+
a. une chaîne de caractères pouvant contenir du HTML
|
53
|
+
b. une fonction callback recevant comme unique argument un objet JS de type :
|
54
|
+
{
|
55
|
+
title: "xx",
|
56
|
+
description: "xxxxx",
|
57
|
+
image: "http://example.com/pic.jpg"
|
58
|
+
}
|
59
|
+
2. La méthode parse du client s'occupe de récupérer la première URL contenue dans le chaîne de caractère passée en argument
|
60
|
+
3. S'il s'agit d'une nouvelle URL, il envoie une requête $.post contenant l'URL extraite au service
|
61
|
+
4. Le retour de la requête est traitée :
|
62
|
+
a. Si le serveur retourne un code HTTP 200, le contenu de la réponse JSON est parsé et le callback appelé
|
63
|
+
b. Si le serveur retourne un code HTTP 404,
|
64
|
+
|
65
|
+
|
66
|
+
Example d'utilisation du plugin:
|
67
|
+
|
68
|
+
// On indique au parser l'URL du service
|
69
|
+
ContentPreview.service_url = 'http://example.com/content-preview-service'
|
70
|
+
var $textarea = $('textarea#user-content-input')
|
71
|
+
|
72
|
+
$textarea.on('keyup', function() {
|
73
|
+
$textarea.parseContentPreview($textarea.val(), function(resp) {
|
74
|
+
// Traitement de la réponse
|
75
|
+
});
|
76
|
+
});
|
77
|
+
|
78
|
+
----------------------------------------------------------------------
|
79
|
+
|
80
|
+
V.1 :
|
81
|
+
|
82
|
+
Une fois le service et le client JS testés complètement
|
83
|
+
|
84
|
+
----------------------------------------------------------------------
|
85
|
+
|
86
|
+
Idées supplémentaires :
|
87
|
+
|
88
|
+
- Mise en place d'un cache (probablement avec Redis) permettant de stocker le hash de réponse et de ne pas rechecker une URL si celle-ci a déjà été stockée. (Peut être avec un EXPIRE en fonction du cache ... à voir)
|
89
|
+
- Authentification au niveau du service
|
90
|
+
|
@@ -0,0 +1,65 @@
|
|
1
|
+
#encoding: UTF-8!
|
2
|
+
require 'nokogiri'
|
3
|
+
require 'open-uri'
|
4
|
+
|
5
|
+
require_relative '../../lib/content-preview'
|
6
|
+
|
7
|
+
describe ContentPreview::Parser, '::process' do
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@content_preview = ContentPreview::Parser.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'should respond to %w(title description pictures) attributes' do
|
14
|
+
@content_preview.should respond_to :title
|
15
|
+
@content_preview.should respond_to :description
|
16
|
+
@content_preview.should respond_to :images
|
17
|
+
end
|
18
|
+
|
19
|
+
describe ContentPreview::Parser, 'pictures attribute' do
|
20
|
+
it 'should be a list' do
|
21
|
+
@content_preview.images.should be_kind_of Array
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should not return nil if the url is correct' do
|
26
|
+
preview = @content_preview.process "http://www.google.com"
|
27
|
+
preview.should_not be_nil
|
28
|
+
end
|
29
|
+
|
30
|
+
it "returns nil when url isn't well formatted" do
|
31
|
+
preview = %w(example.com example http:/www.example.com www.example.com).reduce(nil) do |result, url|
|
32
|
+
result = result || @content_preview.process(url) != nil ? true : nil
|
33
|
+
end
|
34
|
+
preview.should be_nil
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns nil when domain name doesn't exist" do
|
38
|
+
preview = @content_preview.process "http://www.tezzdazfazfadada.dza"
|
39
|
+
preview.should be_nil
|
40
|
+
end
|
41
|
+
|
42
|
+
it "returns nil if URL can't be found by the remote server" do
|
43
|
+
preview = @content_preview.process "http://www.google.com/oidajzodjaozjcaozcaozcnazocna"
|
44
|
+
preview.should be_nil
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should return valid information when processed with Google url' do
|
48
|
+
preview = @content_preview.process "http://www.google.com"
|
49
|
+
preview.should_not be_nil
|
50
|
+
preview.should include({"title"=>"Google", "description"=>nil, "images"=>[]})
|
51
|
+
|
52
|
+
@content_preview.title.should_not be_nil
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'should return valid attributes when processed with a Youtube video url' do
|
56
|
+
preview = @content_preview.process "http://www.youtube.com/watch?v=OK7pfLlsUQM"
|
57
|
+
preview.should_not be_nil
|
58
|
+
preview.should include(
|
59
|
+
{"title"=>"The Artist - Official Trailer [HD]", "description"=>"Subscribe http://ow.ly/3UVvY | Facebook http://ow.ly/3UVxn | Twitter http://ow.ly/3UVyA Release Date: 23 November 2011 Genre: Romance | Comedy | Drama Cast: ...", "images"=>["http://i4.ytimg.com/vi/OK7pfLlsUQM/mqdefault.jpg"]}
|
60
|
+
)
|
61
|
+
|
62
|
+
@content_preview.title.should_not be_nil
|
63
|
+
@content_preview.images.should_not be_empty
|
64
|
+
end
|
65
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
|
+
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
|
+
# Require this file using `require "spec_helper"` to ensure that it is only
|
4
|
+
# loaded once.
|
5
|
+
#
|
6
|
+
# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
|
7
|
+
RSpec.configure do |config|
|
8
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
9
|
+
config.run_all_when_everything_filtered = true
|
10
|
+
config.filter_run :focus
|
11
|
+
|
12
|
+
# Run specs in random order to surface order dependencies. If you find an
|
13
|
+
# order dependency and want to debug it, you can fix the order by providing
|
14
|
+
# the seed, which is printed after each run.
|
15
|
+
# --seed 1234
|
16
|
+
config.order = 'random'
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: content-preview
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Glyph
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: thor
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: nokogiri
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rack-cors
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :runtime
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rack-test
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :runtime
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: json
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :runtime
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
description: A simple service for getting URL page informations
|
111
|
+
email:
|
112
|
+
- vala@glyph.fr
|
113
|
+
executables:
|
114
|
+
- cp-server
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- .gitignore
|
119
|
+
- .rspec
|
120
|
+
- Gemfile
|
121
|
+
- Gemfile.lock
|
122
|
+
- MIT-LICENSE
|
123
|
+
- README.md
|
124
|
+
- Rakefile
|
125
|
+
- bin/cp-server
|
126
|
+
- config.ru
|
127
|
+
- content-preview.gemspec
|
128
|
+
- js/src/content-preview.coffee
|
129
|
+
- lib/content-preview.rb
|
130
|
+
- lib/content-preview/parser.rb
|
131
|
+
- lib/content-preview/router.rb
|
132
|
+
- lib/content-preview/router/handlers/default.rb
|
133
|
+
- lib/content-preview/router/router.rb
|
134
|
+
- lib/content-preview/router/routes.yml
|
135
|
+
- lib/content-preview/version.rb
|
136
|
+
- lib/tasks/content-preview_tasks.rake
|
137
|
+
- roadmap.txt
|
138
|
+
- spec/parser/parser_spec.rb
|
139
|
+
- spec/spec_helper.rb
|
140
|
+
homepage: http://glyph.fr
|
141
|
+
licenses: []
|
142
|
+
post_install_message:
|
143
|
+
rdoc_options: []
|
144
|
+
require_paths:
|
145
|
+
- lib
|
146
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
147
|
+
none: false
|
148
|
+
requirements:
|
149
|
+
- - ! '>='
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
segments:
|
153
|
+
- 0
|
154
|
+
hash: 1542849611858236016
|
155
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
156
|
+
none: false
|
157
|
+
requirements:
|
158
|
+
- - ! '>='
|
159
|
+
- !ruby/object:Gem::Version
|
160
|
+
version: '0'
|
161
|
+
segments:
|
162
|
+
- 0
|
163
|
+
hash: 1542849611858236016
|
164
|
+
requirements: []
|
165
|
+
rubyforge_project:
|
166
|
+
rubygems_version: 1.8.23
|
167
|
+
signing_key:
|
168
|
+
specification_version: 3
|
169
|
+
summary: A simple service for getting URL page informations
|
170
|
+
test_files: []
|