content-preview 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|