scrivito_sdk 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.yardopts +5 -0
- data/README +6 -0
- data/app/controllers/cms_controller.rb +7 -0
- data/app/controllers/scrivito/blobs_controller.rb +10 -0
- data/app/controllers/scrivito/default_cms_controller.rb +61 -0
- data/app/controllers/scrivito/objs_controller.rb +200 -0
- data/app/controllers/scrivito/tasks_controller.rb +11 -0
- data/app/controllers/scrivito/webservice_controller.rb +36 -0
- data/app/controllers/scrivito/workspaces_controller.rb +41 -0
- data/app/helpers/cms_helper.rb +7 -0
- data/app/helpers/cms_routing_helper.rb +7 -0
- data/app/helpers/scrivito/cms_asset_helper.rb +103 -0
- data/app/helpers/scrivito/cms_tag_helper.rb +231 -0
- data/app/helpers/scrivito/default_cms_helper.rb +21 -0
- data/app/helpers/scrivito/default_cms_routing_helper.rb +130 -0
- data/app/helpers/scrivito/display_helper.rb +71 -0
- data/app/helpers/scrivito/editing_helper.rb +26 -0
- data/app/helpers/scrivito/layout_helper.rb +28 -0
- data/app/models/named_link.rb +2 -0
- data/app/views/cms/_index.html.erb +7 -0
- data/app/views/cms/index.html.erb +1 -0
- data/app/views/scrivito/_editing_javascript.html.erb +7 -0
- data/app/views/scrivito/default_cms/show_widget.html.erb +1 -0
- data/app/views/scrivito/objs/copy_widget.html.erb +1 -0
- data/app/views/scrivito/objs/create_widget.html.erb +1 -0
- data/app/views/scrivito/widget_thumbnail.html.erb +9 -0
- data/config/ca-bundle.crt +3509 -0
- data/config/cms_routes.rb +17 -0
- data/config/locales/de.scrivito.errors.yml +7 -0
- data/config/locales/de.scrivito.lib.yml +6 -0
- data/config/locales/de.scrivito.models.yml +6 -0
- data/config/locales/en.scrivito.errors.yml +7 -0
- data/config/locales/en.scrivito.lib.yml +6 -0
- data/config/locales/en.scrivito.models.yml +6 -0
- data/config/routes.rb +37 -0
- data/lib/assets/images/180x120.gif +0 -0
- data/lib/assets/images/scrivito/image_placeholder.png +0 -0
- data/lib/assets/javascripts/scrivito_editing.js +14642 -0
- data/lib/assets/stylesheets/scrivito.css +180 -0
- data/lib/assets/stylesheets/scrivito_editing.css +2213 -0
- data/lib/generators/cms/migration/USAGE +9 -0
- data/lib/generators/cms/migration/migration_generator.rb +21 -0
- data/lib/generators/cms/migration/templates/migration.erb +10 -0
- data/lib/obj.rb +3 -0
- data/lib/scrivito/access_denied.rb +6 -0
- data/lib/scrivito/attribute_content.rb +194 -0
- data/lib/scrivito/backend_error.rb +4 -0
- data/lib/scrivito/basic_obj.rb +840 -0
- data/lib/scrivito/basic_widget.rb +238 -0
- data/lib/scrivito/blob.rb +48 -0
- data/lib/scrivito/cache.rb +41 -0
- data/lib/scrivito/cache_garbage_collector.rb +83 -0
- data/lib/scrivito/cache_middleware.rb +17 -0
- data/lib/scrivito/client_config.rb +62 -0
- data/lib/scrivito/client_error.rb +12 -0
- data/lib/scrivito/cms_accessible.rb +30 -0
- data/lib/scrivito/cms_backend.rb +238 -0
- data/lib/scrivito/cms_cache_storage.rb +51 -0
- data/lib/scrivito/cms_dispatch_controller.rb +46 -0
- data/lib/scrivito/cms_env.rb +63 -0
- data/lib/scrivito/cms_field_tag.rb +112 -0
- data/lib/scrivito/cms_rest_api.rb +151 -0
- data/lib/scrivito/cms_rest_api/attribute_serializer.rb +98 -0
- data/lib/scrivito/cms_rest_api/blob_uploader.rb +18 -0
- data/lib/scrivito/cms_rest_api/widget_extractor.rb +42 -0
- data/lib/scrivito/cms_test_request.rb +23 -0
- data/lib/scrivito/communication_error.rb +17 -0
- data/lib/scrivito/comparison.rb +67 -0
- data/lib/scrivito/configuration.rb +221 -0
- data/lib/scrivito/connection_manager.rb +100 -0
- data/lib/scrivito/content_conversion.rb +43 -0
- data/lib/scrivito/content_service.rb +118 -0
- data/lib/scrivito/content_state.rb +109 -0
- data/lib/scrivito/content_state_caching.rb +47 -0
- data/lib/scrivito/content_state_visitor.rb +19 -0
- data/lib/scrivito/controller_runtime.rb +35 -0
- data/lib/scrivito/date_attribute.rb +16 -0
- data/lib/scrivito/deprecation.rb +21 -0
- data/lib/scrivito/diff.rb +110 -0
- data/lib/scrivito/editing_context.rb +106 -0
- data/lib/scrivito/editing_context_middleware.rb +60 -0
- data/lib/scrivito/engine.rb +65 -0
- data/lib/scrivito/errors.rb +11 -0
- data/lib/scrivito/html_string.rb +18 -0
- data/lib/scrivito/link.rb +187 -0
- data/lib/scrivito/link_parser.rb +81 -0
- data/lib/scrivito/log_subscriber.rb +29 -0
- data/lib/scrivito/migration.rb +2 -0
- data/lib/scrivito/migrations.rb +12 -0
- data/lib/scrivito/migrations/cms_backend.rb +94 -0
- data/lib/scrivito/migrations/installer.rb +45 -0
- data/lib/scrivito/migrations/migration.rb +93 -0
- data/lib/scrivito/migrations/migration_dsl.rb +143 -0
- data/lib/scrivito/migrations/migration_store.rb +23 -0
- data/lib/scrivito/migrations/migrator.rb +135 -0
- data/lib/scrivito/migrations/workspace_lock.rb +39 -0
- data/lib/scrivito/model_identity.rb +13 -0
- data/lib/scrivito/modification.rb +8 -0
- data/lib/scrivito/named_link.rb +75 -0
- data/lib/scrivito/network_error.rb +11 -0
- data/lib/scrivito/obj_data.rb +140 -0
- data/lib/scrivito/obj_data_from_hash.rb +31 -0
- data/lib/scrivito/obj_data_from_service.rb +84 -0
- data/lib/scrivito/obj_params_parser.rb +61 -0
- data/lib/scrivito/obj_search_builder.rb +62 -0
- data/lib/scrivito/obj_search_enumerator.rb +374 -0
- data/lib/scrivito/rate_limit_exceeded.rb +5 -0
- data/lib/scrivito/revision.rb +9 -0
- data/lib/scrivito/string_tagging.rb +18 -0
- data/lib/scrivito/text_link.rb +52 -0
- data/lib/scrivito/text_link_conversion.rb +52 -0
- data/lib/scrivito/type_computer.rb +34 -0
- data/lib/scrivito/widget_field_params.rb +61 -0
- data/lib/scrivito/widget_garbage_collection.rb +97 -0
- data/lib/scrivito/workspace.rb +222 -0
- data/lib/scrivito/workspace_data_from_service.rb +80 -0
- data/lib/scrivito/workspace_selection_middleware.rb +23 -0
- data/lib/scrivito_sdk.rb +19 -0
- data/lib/tasks/cache.rake +12 -0
- data/lib/tasks/migration.rake +35 -0
- data/lib/widget.rb +3 -0
- metadata +291 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
module Scrivito
|
5
|
+
class ContentService
|
6
|
+
class RateLimitExceeded < StandardError
|
7
|
+
def initialize(retry_after)
|
8
|
+
@retry_after = retry_after
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :retry_after
|
12
|
+
end
|
13
|
+
|
14
|
+
class MaxSleepTimeReached < StandardError
|
15
|
+
end
|
16
|
+
|
17
|
+
class Delay
|
18
|
+
def initialize
|
19
|
+
@sleep_count = 0
|
20
|
+
@total_sleep_time = 0
|
21
|
+
end
|
22
|
+
|
23
|
+
def next_sleep_time(min_sleep = 0)
|
24
|
+
sleep_time = [2 ** @sleep_count * 0.5, min_sleep.to_f].max
|
25
|
+
@total_sleep_time += sleep_time
|
26
|
+
raise MaxSleepTimeReached if @total_sleep_time > 40
|
27
|
+
@sleep_count += 1
|
28
|
+
sleep_time
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class << self
|
33
|
+
@next_request_not_before = nil
|
34
|
+
|
35
|
+
def query(path, payload, options={})
|
36
|
+
timeout = options.fetch(:timeout, ConnectionManager::DEFAULT_TIMEOUT)
|
37
|
+
retry_until_timeout_on_rate_limit_exceeded do
|
38
|
+
request = build_request(path, payload)
|
39
|
+
if @next_request_not_before && @next_request_not_before > (now = Time.now)
|
40
|
+
sleep @next_request_not_before - now
|
41
|
+
end
|
42
|
+
|
43
|
+
response = nil
|
44
|
+
retry_once_on_network_error do
|
45
|
+
response = connection_manager.request(request, timeout)
|
46
|
+
end
|
47
|
+
handle_response(response)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# for tests only
|
52
|
+
def forget_retry_after
|
53
|
+
@next_request_not_before = nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def connection_manager
|
57
|
+
ConnectionManager.instance
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
def retry_once_on_network_error
|
63
|
+
retried = false
|
64
|
+
begin
|
65
|
+
yield
|
66
|
+
rescue NetworkError => e
|
67
|
+
raise e if retried
|
68
|
+
retried = true
|
69
|
+
retry
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def retry_until_timeout_on_rate_limit_exceeded
|
74
|
+
delay = Delay.new
|
75
|
+
begin
|
76
|
+
yield
|
77
|
+
rescue RateLimitExceeded => e
|
78
|
+
begin
|
79
|
+
sleep delay.next_sleep_time(e.retry_after)
|
80
|
+
rescue MaxSleepTimeReached
|
81
|
+
raise ::Scrivito::RateLimitExceeded.new('rate limit exceeded', 429)
|
82
|
+
end
|
83
|
+
retry
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_request(path, payload)
|
88
|
+
url = "#{Configuration.endpoint_uri.path}/#{path}".squeeze('/')
|
89
|
+
request = Net::HTTP::Post.new(url, request_headers)
|
90
|
+
request.basic_auth('api_token', Configuration.api_key)
|
91
|
+
request.body = MultiJson.dump(payload)
|
92
|
+
request
|
93
|
+
end
|
94
|
+
|
95
|
+
def handle_response(response)
|
96
|
+
if response.code.first == '2'
|
97
|
+
retry_after = response["Retry-After"]
|
98
|
+
@next_request_not_before = retry_after ? Time.now + retry_after.to_f : nil
|
99
|
+
MultiJson.load(response.body)
|
100
|
+
elsif response.code == "429"
|
101
|
+
raise RateLimitExceeded.new(response["Retry-After"])
|
102
|
+
elsif response.code == '403'
|
103
|
+
raise AccessDenied.new(response.body)
|
104
|
+
else
|
105
|
+
raise NetworkError.new("Server responded with status code #{response.code}", response.code)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def request_headers
|
110
|
+
{
|
111
|
+
'Accept' => 'application/json',
|
112
|
+
'Content-Type' => 'application/json',
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Scrivito
|
2
|
+
|
3
|
+
# This class represents a single instance of a content state.
|
4
|
+
# Basically a content state has three important characteristics: cache with obj datas,
|
5
|
+
# changes representing changed objs and ways they has been changed and an ancestor content state.
|
6
|
+
class ContentState < Struct.new(:content_state_id, :changes, :changes_index, :from_content_state_id)
|
7
|
+
class << self
|
8
|
+
private :new
|
9
|
+
|
10
|
+
# Creates a new content state with given changes and ancestor (optional).
|
11
|
+
def create(attributes)
|
12
|
+
new(attributes).tap do |content_state|
|
13
|
+
content_state.index_changes!
|
14
|
+
CmsCacheStorage.write_content_state(content_state.content_state_id, content_state.to_hash)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# Finds a previously saved content state.
|
19
|
+
# Returns nil if not found.
|
20
|
+
def find(content_state_id)
|
21
|
+
if content_state_data = CmsCacheStorage.read_content_state(content_state_id)
|
22
|
+
new(content_state_data)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Fetches an existing workspace.
|
27
|
+
# If not found creates a new one and returns it.
|
28
|
+
def find_or_create(content_state_id)
|
29
|
+
find(content_state_id) || create(content_state_id: content_state_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize(attributes)
|
34
|
+
super(*attributes.symbolize_keys.values_at(:content_state_id, :changes, :changes_index,
|
35
|
+
:from_content_state_id))
|
36
|
+
end
|
37
|
+
|
38
|
+
# Stores arbitrary data in cache.
|
39
|
+
# Cache key is build from given index and key.
|
40
|
+
def save_obj_data(index, key, data)
|
41
|
+
CmsCacheStorage.write_obj_data(content_state_id, index, key, data)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Fetches previously stored arbitrary data from cache.
|
45
|
+
# Returns nil if nothing found.
|
46
|
+
# Cache key is build from given index and key.
|
47
|
+
def find_obj_data(index, key)
|
48
|
+
CmsCacheStorage.read_obj_data(content_state_id, index, key)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Fetches and caches the ancestor.
|
52
|
+
# Returns nil if there is no ancestor.
|
53
|
+
def from_content_state
|
54
|
+
@from_content_state ||= self.class.find(from_content_state_id)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Determines whether given data is still up-to-date for given index and key.
|
58
|
+
def has_changes_for?(index, key, data)
|
59
|
+
case index
|
60
|
+
when 'id'
|
61
|
+
id_index.include?(key)
|
62
|
+
when 'path'
|
63
|
+
path_index.include?(key) || data.first && id_index.include?(data.first['_id'].first)
|
64
|
+
when 'ppath'
|
65
|
+
ppath_index.include?(key) || data.find { |d| id_index.include?(d['_id'].first) }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Computes for a given changes feed a set of access-efficient indexes.
|
70
|
+
def index_changes!
|
71
|
+
id_index, path_index, ppath_index = Set.new, Set.new, Set.new
|
72
|
+
if changes.present?
|
73
|
+
changes.each do |hash|
|
74
|
+
id_index.add(hash['id'])
|
75
|
+
if path = hash['modified_path']
|
76
|
+
path_index.add(path)
|
77
|
+
ppath_index.add(path.gsub(/\/[^\/]+$/, '').presence || '/') if path != '/'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
self.changes = nil
|
82
|
+
self.changes_index = {'id' => id_index, 'path' => path_index, 'ppath' => ppath_index}
|
83
|
+
end
|
84
|
+
|
85
|
+
# Returns a hash representation of a content state for serialization purpose.
|
86
|
+
def to_hash
|
87
|
+
{
|
88
|
+
content_state_id: content_state_id,
|
89
|
+
changes_index: changes_index,
|
90
|
+
from_content_state_id: from_content_state_id
|
91
|
+
}
|
92
|
+
end
|
93
|
+
|
94
|
+
private
|
95
|
+
|
96
|
+
def id_index
|
97
|
+
@id_index ||= changes_index['id']
|
98
|
+
end
|
99
|
+
|
100
|
+
def path_index
|
101
|
+
@path_index ||= changes_index['path']
|
102
|
+
end
|
103
|
+
|
104
|
+
def ppath_index
|
105
|
+
@ppath_index ||= changes_index['ppath']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Scrivito
|
2
|
+
|
3
|
+
# This module provides advances auto-invalidating caching mechanism for storing obj data.
|
4
|
+
#
|
5
|
+
# To keep it up-to-date its caches and changes should be updated periodically
|
6
|
+
# It's changes should be updated every time a new workspace data has been fetched.
|
7
|
+
# It's caches should be updated every time a new obj data has been fetched.
|
8
|
+
module ContentStateCaching
|
9
|
+
class << self
|
10
|
+
# How deep should a content state chain be inspected. Default depth is 20.
|
11
|
+
attr_accessor :cache_lookup_depth
|
12
|
+
|
13
|
+
# At which lookup depth to copy a hit found in an ancestor content state
|
14
|
+
# to the current content state's cache. Default depth is 5.
|
15
|
+
attr_accessor :cache_replication_depth
|
16
|
+
|
17
|
+
# Updates caches with data from given workspace.
|
18
|
+
# Should be called every time a new OBJ data has been fetched.
|
19
|
+
def store_obj_data(content_state, index, key, data)
|
20
|
+
content_state.save_obj_data(index, key, data) if content_state
|
21
|
+
end
|
22
|
+
|
23
|
+
# Fetches up-to-date obj data for given workspace, index and key.
|
24
|
+
# Returns nil if no up-to-date data found.
|
25
|
+
def find_obj_data(current_content_state , index, key)
|
26
|
+
if index == 'permalink'
|
27
|
+
current_content_state.find_obj_data(index, key)
|
28
|
+
else
|
29
|
+
visitor = ContentStateVisitor.new(current_content_state)
|
30
|
+
cache_lookup_depth.times do |depth|
|
31
|
+
return unless content_state = visitor.visit_next
|
32
|
+
if hit = content_state.find_obj_data(index, key)
|
33
|
+
visitor.visited_except_current.each { |cs| return if cs.has_changes_for?(index, key, hit) }
|
34
|
+
current_content_state.save_obj_data(index, key, hit) if depth >= cache_replication_depth
|
35
|
+
return hit
|
36
|
+
end
|
37
|
+
end
|
38
|
+
nil
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
self.cache_replication_depth = 5
|
44
|
+
self.cache_lookup_depth = 20
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module Scrivito
|
2
|
+
class ContentStateVisitor
|
3
|
+
def initialize(start_content_state)
|
4
|
+
@next_content_state, @visited = start_content_state, []
|
5
|
+
end
|
6
|
+
|
7
|
+
def visit_next
|
8
|
+
if content_state = @next_content_state
|
9
|
+
@visited << content_state
|
10
|
+
@next_content_state = content_state.from_content_state
|
11
|
+
content_state
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def visited_except_current
|
16
|
+
@visited[0..-2]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'active_support/core_ext/module/attr_internal'
|
2
|
+
|
3
|
+
module Scrivito
|
4
|
+
|
5
|
+
module ControllerRuntime
|
6
|
+
extend ActiveSupport::Concern
|
7
|
+
|
8
|
+
protected
|
9
|
+
|
10
|
+
attr_internal :rc_runtime
|
11
|
+
|
12
|
+
def cleanup_view_runtime
|
13
|
+
rc_rt_before_render = Scrivito::LogSubscriber.reset_runtime
|
14
|
+
runtime = super
|
15
|
+
rc_rt_after_render = Scrivito::LogSubscriber.reset_runtime
|
16
|
+
self.rc_runtime = rc_rt_before_render + rc_rt_after_render
|
17
|
+
runtime - rc_rt_after_render
|
18
|
+
end
|
19
|
+
|
20
|
+
def append_info_to_payload(payload)
|
21
|
+
super
|
22
|
+
payload[:rc_runtime] = rc_runtime
|
23
|
+
end
|
24
|
+
|
25
|
+
module ClassMethods
|
26
|
+
def log_process_action(payload)
|
27
|
+
messages, rc_runtime = super, payload[:rc_runtime]
|
28
|
+
messages << ("Scrivito: %.1fms" % rc_runtime.to_f)
|
29
|
+
messages
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end # module Scrivito
|
35
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Scrivito
|
2
|
+
|
3
|
+
# Adds support for string columns which contain ISO dates
|
4
|
+
module DateAttribute
|
5
|
+
def self.parse(iso_date_time)
|
6
|
+
return nil unless iso_date_time
|
7
|
+
|
8
|
+
if iso_date_time.to_s =~ /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/
|
9
|
+
Time.utc($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i).in_time_zone
|
10
|
+
else
|
11
|
+
raise "The value is not a valid ISO date time: #{iso_date_time.inspect}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Scrivito
|
2
|
+
class Deprecation
|
3
|
+
class << self
|
4
|
+
def warn(message, source = caller(1))
|
5
|
+
ActiveSupport::Deprecation.warn(message, source)
|
6
|
+
end
|
7
|
+
|
8
|
+
def warn_method(depricated_method_name, new_method_name = nil)
|
9
|
+
warn(warn_method_message(depricated_method_name, new_method_name), caller(1))
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def warn_method_message(depricated_method_name, new_method_name = nil)
|
15
|
+
message = "The method #{depricated_method_name} will be removed from the Scrivito SDK. "
|
16
|
+
message << "Please use #{new_method_name} instead" if new_method_name
|
17
|
+
message
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "diff-lcs"
|
2
|
+
require "hyp_diff"
|
3
|
+
|
4
|
+
module Scrivito
|
5
|
+
|
6
|
+
class Diff; class << self
|
7
|
+
|
8
|
+
def for(mode, field_type, old, new)
|
9
|
+
case field_type
|
10
|
+
when "widget"
|
11
|
+
ListComparison.for(mode, old || [], new || [])
|
12
|
+
when "html"
|
13
|
+
for_html(mode, old, new)
|
14
|
+
when "text"
|
15
|
+
for_string(mode, old, new)
|
16
|
+
when "string"
|
17
|
+
for_string(mode, old, new)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def for_string(mode, old, new)
|
24
|
+
for_html(mode, ERB::Util.h(old), ERB::Util.h(new))
|
25
|
+
end
|
26
|
+
|
27
|
+
def for_html(mode, old, new)
|
28
|
+
return nil if !old
|
29
|
+
return nil if !new
|
30
|
+
|
31
|
+
hyp_diff_options =
|
32
|
+
case mode
|
33
|
+
when "added"
|
34
|
+
{render_deletion: proc {}, markup_from: "after"}
|
35
|
+
when "deleted"
|
36
|
+
{render_insertion: proc {}, markup_from: "before"}
|
37
|
+
when "diff"
|
38
|
+
{}
|
39
|
+
else
|
40
|
+
return nil
|
41
|
+
end
|
42
|
+
|
43
|
+
diff = HypDiff.compare(old, new, hyp_diff_options)
|
44
|
+
[StringTagging.tag_as_html(diff)]
|
45
|
+
end
|
46
|
+
|
47
|
+
class ListComparison
|
48
|
+
def self.for(mode, old, new)
|
49
|
+
new(mode).compare(old, new).result
|
50
|
+
end
|
51
|
+
|
52
|
+
def initialize(mode)
|
53
|
+
@mode = mode
|
54
|
+
@list = []
|
55
|
+
@modifications = []
|
56
|
+
end
|
57
|
+
|
58
|
+
def compare(old, new)
|
59
|
+
::Diff::LCS.sdiff(old, new).map do |change|
|
60
|
+
case change.action
|
61
|
+
when "-" then
|
62
|
+
note_deletion(change)
|
63
|
+
when "+" then
|
64
|
+
note_insertion(change)
|
65
|
+
when "=" then
|
66
|
+
note_equal(change)
|
67
|
+
when "!" then
|
68
|
+
note_deletion(change)
|
69
|
+
note_insertion(change)
|
70
|
+
else
|
71
|
+
raise "unexpected change.action #{change.action}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
self
|
76
|
+
end
|
77
|
+
|
78
|
+
def result
|
79
|
+
[@list, @modifications]
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def note_equal(change)
|
85
|
+
@modifications << nil
|
86
|
+
if @mode == "deleted"
|
87
|
+
@list << change.old_element
|
88
|
+
else
|
89
|
+
@list << change.new_element
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def note_deletion(change)
|
94
|
+
unless @mode == "added"
|
95
|
+
@modifications << "deleted"
|
96
|
+
@list << change.old_element
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def note_insertion(change)
|
101
|
+
unless @mode == "deleted"
|
102
|
+
@modifications << "new"
|
103
|
+
@list << change.new_element
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end; end
|
109
|
+
|
110
|
+
end
|