card 1.18.0 → 1.18.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/card.gemspec +20 -16
- data/db/migrate_core_cards/20150202143810_import_bootstrap_layout.rb +1 -1
- data/db/schema.rb +110 -92
- data/lib/card.rb +1 -0
- data/lib/card/content.rb +4 -73
- data/lib/card/content/chunk.rb +119 -0
- data/lib/card/content/parser.rb +75 -0
- data/lib/card/diff.rb +25 -398
- data/lib/card/diff/lcs.rb +247 -0
- data/lib/card/diff/result.rb +131 -0
- data/lib/card/director_register.rb +5 -0
- data/lib/card/query/attributes.rb +19 -13
- data/lib/card/set/event.rb +2 -1
- data/lib/card/set_pattern.rb +4 -2
- data/lib/card/spec_helper.rb +7 -1
- data/lib/card/stage_director.rb +33 -5
- data/lib/card/subcards.rb +11 -3
- data/lib/card/subdirector_array.rb +14 -1
- data/lib/cardio.rb +8 -5
- data/mod/01_core/chunk/include.rb +2 -2
- data/mod/01_core/chunk/link.rb +3 -3
- data/mod/01_core/chunk/literal.rb +20 -14
- data/mod/01_core/chunk/query_reference.rb +2 -2
- data/mod/01_core/chunk/reference.rb +47 -38
- data/mod/01_core/chunk/uri.rb +17 -13
- data/mod/01_core/format/html_format.rb +0 -2
- data/mod/01_core/set/all/actify.rb +12 -1
- data/mod/01_core/set/all/collection.rb +4 -4
- data/mod/01_core/set/all/fetch.rb +0 -27
- data/mod/01_core/set/all/name.rb +33 -12
- data/mod/01_core/set/all/pattern.rb +2 -6
- data/mod/01_core/set/all/phases.rb +0 -1
- data/mod/01_core/set/all/references.rb +2 -2
- data/mod/01_core/set/all/rules.rb +10 -3
- data/mod/01_core/set/all/tracked_attributes.rb +0 -1
- data/mod/01_core/set/all/type.rb +0 -14
- data/mod/01_core/spec/chunk/literal_spec.rb +1 -1
- data/mod/01_core/spec/chunk/uri_spec.rb +204 -201
- data/mod/01_core/spec/set/all/type_spec.rb +3 -1
- data/mod/01_history/lib/card/action.rb +7 -9
- data/mod/01_history/set/all/history.rb +6 -1
- data/mod/02_basic_types/set/all/all_csv.rb +1 -1
- data/mod/02_basic_types/set/type/pointer.rb +20 -9
- data/mod/03_machines/lib/javascript/wagn.js.coffee +1 -1
- data/mod/04_settings/set/right/structure.rb +7 -1
- data/mod/05_email/set/right/follow.rb +22 -22
- data/mod/05_email/set/type_plus_right/user/follow.rb +25 -26
- data/mod/05_standard/set/all/rich_html/wrapper.rb +12 -6
- data/mod/05_standard/set/rstar/rules_editor.rb +6 -4
- data/mod/05_standard/set/self/all.rb +0 -10
- data/mod/05_standard/set/self/stats.rb +6 -15
- data/mod/05_standard/set/type/set.rb +0 -6
- data/mod/05_standard/spec/chunk/include_spec.rb +2 -2
- data/mod/05_standard/spec/chunk/link_spec.rb +1 -1
- data/mod/05_standard/spec/chunk/query_reference_spec.rb +5 -4
- data/spec/lib/card/chunk_spec.rb +7 -5
- data/spec/lib/card/content_spec.rb +11 -11
- data/spec/lib/card/diff_spec.rb +4 -4
- data/spec/lib/card/stage_director_spec.rb +56 -0
- data/spec/lib/card/subcards_spec.rb +0 -1
- data/spec/models/card/type_transition_spec.rb +5 -42
- metadata +12 -23
- data/lib/card/chunk.rb +0 -122
@@ -0,0 +1,247 @@
|
|
1
|
+
class Card
|
2
|
+
class Diff
|
3
|
+
# Use LCS algorithm to create a Diff::Result
|
4
|
+
class LCS
|
5
|
+
def initialize opts
|
6
|
+
# regex; remove matches completely from diff
|
7
|
+
@reject_pattern = opts[:reject]
|
8
|
+
# regex; put matches back to the result after diff
|
9
|
+
@exclude_pattern = opts[:exclude]
|
10
|
+
|
11
|
+
@preprocess = opts[:preprocess] # block; called with every word
|
12
|
+
@postprocess = opts[:postprocess] # block; called with complete diff
|
13
|
+
|
14
|
+
@splitters = %w( <[^>]+> \[\[[^\]]+\]\] \{\{[^}]+\}\} \s+ )
|
15
|
+
@disjunction_pattern = /^\s/
|
16
|
+
end
|
17
|
+
|
18
|
+
def run old_text, new_text, result
|
19
|
+
@result = result
|
20
|
+
compare old_text, new_text
|
21
|
+
@result.complete = postprocess @result.complete
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def compare old_text, new_text
|
27
|
+
if old_text
|
28
|
+
old_words, old_ex = separate_comparables_from_excludees old_text
|
29
|
+
new_words, new_ex = separate_comparables_from_excludees new_text
|
30
|
+
ChunkProcessor.new(old_words, new_words, old_ex, new_ex).run(@result)
|
31
|
+
else
|
32
|
+
list = split_and_preprocess(new_text)
|
33
|
+
if @exclude_pattern
|
34
|
+
list = list.reject { |word| word.match @exclude_pattern }
|
35
|
+
end
|
36
|
+
# CAUTION: postproces and added_chunk changed order
|
37
|
+
# and no longer postprocess for summary
|
38
|
+
@result.write_added_chunk list.join
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def separate_comparables_from_excludees text
|
43
|
+
# return two arrays, one with all words, one with pairs
|
44
|
+
# (index in word list, html_tag)
|
45
|
+
list = split_and_preprocess text
|
46
|
+
if @exclude_pattern
|
47
|
+
check_exclude_and_disjunction_pattern list
|
48
|
+
else
|
49
|
+
[list, []]
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def check_exclude_and_disjunction_pattern list
|
54
|
+
list.each_with_index.each_with_object([[], []]) do |pair, res|
|
55
|
+
element, index = pair
|
56
|
+
if element.match @disjunction_pattern
|
57
|
+
res[1] << { chunk_index: index, element: element,
|
58
|
+
type: :disjunction }
|
59
|
+
elsif element.match @exclude_pattern
|
60
|
+
res[1] << { chunk_index: index, element: element, type:
|
61
|
+
:excludee }
|
62
|
+
else
|
63
|
+
res[0] << element
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def split_and_preprocess text
|
69
|
+
splitted = split_to_list_of_words(text).select do |s|
|
70
|
+
!s.empty? && (!@reject_pattern || !s.match(@reject_pattern))
|
71
|
+
end
|
72
|
+
@preprocess ? splitted.map { |s| @preprocess.call(s) } : splitted
|
73
|
+
end
|
74
|
+
|
75
|
+
def split_to_list_of_words text
|
76
|
+
split_regex = /(#{@splitters.join '|'})/
|
77
|
+
text.split(split_regex)
|
78
|
+
end
|
79
|
+
|
80
|
+
def preprocess text
|
81
|
+
@preprocess ? @preprocess.call(text) : text
|
82
|
+
end
|
83
|
+
|
84
|
+
def postprocess text
|
85
|
+
@postprocess ? @postprocess.call(text) : text
|
86
|
+
end
|
87
|
+
|
88
|
+
# Compares two lists of chunks and generates a diff
|
89
|
+
class ChunkProcessor
|
90
|
+
attr_reader :result, :summary, :dels_cnt, :adds_cnt
|
91
|
+
def initialize old_words, new_words, old_excludees, new_excludees
|
92
|
+
@adds = []
|
93
|
+
@dels = []
|
94
|
+
@words = {
|
95
|
+
old: old_words,
|
96
|
+
new: new_words
|
97
|
+
}
|
98
|
+
@excludees = {
|
99
|
+
old: ExcludeeIterator.new(old_excludees),
|
100
|
+
new: ExcludeeIterator.new(new_excludees)
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
def run result
|
105
|
+
@result = result
|
106
|
+
prev_action = nil
|
107
|
+
::Diff::LCS.traverse_balanced(@words[:old], @words[:new]) do |word|
|
108
|
+
if prev_action
|
109
|
+
if prev_action != word.action &&
|
110
|
+
!(prev_action == '-' && word.action == '!') &&
|
111
|
+
!(prev_action == '!' && word.action == '+')
|
112
|
+
|
113
|
+
# delete and/or add section stops here; write changes to result
|
114
|
+
write_dels
|
115
|
+
write_adds
|
116
|
+
|
117
|
+
# new neutral section starts
|
118
|
+
# we can just write excludees to result
|
119
|
+
write_excludees
|
120
|
+
|
121
|
+
else # current word belongs to edit of previous word
|
122
|
+
case word.action
|
123
|
+
when '-'
|
124
|
+
del_old_excludees
|
125
|
+
when '+'
|
126
|
+
add_new_excludees
|
127
|
+
when '!'
|
128
|
+
del_old_excludees
|
129
|
+
add_new_excludees
|
130
|
+
else
|
131
|
+
write_excludees
|
132
|
+
end
|
133
|
+
end
|
134
|
+
else
|
135
|
+
write_excludees
|
136
|
+
end
|
137
|
+
|
138
|
+
process_word word
|
139
|
+
prev_action = word.action
|
140
|
+
end
|
141
|
+
write_dels
|
142
|
+
write_adds
|
143
|
+
write_excludees
|
144
|
+
|
145
|
+
@result
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def write_unchanged text
|
151
|
+
@result.write_unchanged_chunk text
|
152
|
+
end
|
153
|
+
|
154
|
+
def write_dels
|
155
|
+
return if @dels.empty?
|
156
|
+
@result.write_deleted_chunk @dels.join
|
157
|
+
@dels = []
|
158
|
+
end
|
159
|
+
|
160
|
+
def write_adds
|
161
|
+
return if @adds.empty?
|
162
|
+
@result.write_added_chunk @adds.join
|
163
|
+
@adds = []
|
164
|
+
end
|
165
|
+
|
166
|
+
def write_excludees
|
167
|
+
while (ex = @excludees[:new].next)
|
168
|
+
@result.write_excluded_chunk ex[:element]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def del_old_excludees
|
173
|
+
while (ex = @excludees[:old].next)
|
174
|
+
if ex[:type] == :disjunction
|
175
|
+
@dels << ex[:element]
|
176
|
+
else
|
177
|
+
write_dels
|
178
|
+
@result.write_excluded_chunk ex[:element]
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def add_new_excludees
|
184
|
+
while (ex = @excludees[:new].next)
|
185
|
+
if ex[:type] == :disjunction
|
186
|
+
@adds << ex[:element]
|
187
|
+
else
|
188
|
+
write_adds
|
189
|
+
@result.complete << ex[:element]
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def process_word word
|
195
|
+
process_element word.old_element, word.new_element, word.action
|
196
|
+
end
|
197
|
+
|
198
|
+
def process_element old_element, new_element, action
|
199
|
+
case action
|
200
|
+
when '-'
|
201
|
+
minus old_element
|
202
|
+
when '+'
|
203
|
+
plus new_element
|
204
|
+
when '!'
|
205
|
+
minus old_element
|
206
|
+
plus new_element
|
207
|
+
else
|
208
|
+
write_unchanged new_element
|
209
|
+
@excludees[:new].word_step
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def plus new_element
|
214
|
+
@adds << new_element
|
215
|
+
@excludees[:new].word_step
|
216
|
+
end
|
217
|
+
|
218
|
+
def minus old_element
|
219
|
+
@dels << old_element
|
220
|
+
@excludees[:old].word_step
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class ExcludeeIterator
|
225
|
+
def initialize list
|
226
|
+
@list = list
|
227
|
+
@index = 0
|
228
|
+
@chunk_index = 0
|
229
|
+
end
|
230
|
+
|
231
|
+
def word_step
|
232
|
+
@chunk_index += 1
|
233
|
+
end
|
234
|
+
|
235
|
+
def next
|
236
|
+
if @index < @list.size &&
|
237
|
+
@list[@index][:chunk_index] == @chunk_index
|
238
|
+
res = @list[@index]
|
239
|
+
@index += 1
|
240
|
+
@chunk_index += 1
|
241
|
+
res
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
class Card
|
2
|
+
class Diff
|
3
|
+
class Result
|
4
|
+
attr_accessor :complete, :summary, :dels_cnt, :adds_cnt
|
5
|
+
def initialize summary_opts=nil
|
6
|
+
@dels_cnt = 0
|
7
|
+
@adds_cnt = 0
|
8
|
+
@complete = ''
|
9
|
+
@summary = Summary.new summary_opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def summary
|
13
|
+
@summary.rendered
|
14
|
+
end
|
15
|
+
|
16
|
+
def write_added_chunk text
|
17
|
+
@adds_cnt += 1
|
18
|
+
@complete << Card::Diff.render_added_chunk(text)
|
19
|
+
@summary.add text
|
20
|
+
end
|
21
|
+
|
22
|
+
def write_deleted_chunk text
|
23
|
+
@dels_cnt += 1
|
24
|
+
@complete << Card::Diff.render_deleted_chunk(text)
|
25
|
+
@summary.delete text
|
26
|
+
end
|
27
|
+
|
28
|
+
def write_unchanged_chunk text
|
29
|
+
@complete << text
|
30
|
+
@summary.omit
|
31
|
+
end
|
32
|
+
|
33
|
+
def write_excluded_chunk text
|
34
|
+
@complete << text
|
35
|
+
end
|
36
|
+
|
37
|
+
class Summary
|
38
|
+
def initialize opts
|
39
|
+
opts ||= {}
|
40
|
+
@remaining_chars = opts[:length] || 50
|
41
|
+
@joint = opts[:joint] || '...'
|
42
|
+
|
43
|
+
@summary = nil
|
44
|
+
@chunks = []
|
45
|
+
end
|
46
|
+
|
47
|
+
def rendered
|
48
|
+
@summary ||=
|
49
|
+
begin
|
50
|
+
truncate_overlap
|
51
|
+
@chunks.map do |chunk|
|
52
|
+
render_chunk chunk[:action], chunk[:text]
|
53
|
+
end.join
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def add text
|
58
|
+
add_chunk text, :added
|
59
|
+
end
|
60
|
+
|
61
|
+
def delete text
|
62
|
+
add_chunk text, :deleted
|
63
|
+
end
|
64
|
+
|
65
|
+
def omit
|
66
|
+
if @chunks.empty? || @chunks.last[:action] != :ellipsis
|
67
|
+
add_chunk @joint, :ellipsis
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def add_chunk text, action
|
74
|
+
if @remaining_chars > 0
|
75
|
+
@chunks << { action: action, text: text }
|
76
|
+
@remaining_chars -= text.size
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def render_chunk action, text
|
81
|
+
case action
|
82
|
+
when '+', :added then Card::Diff.render_added_chunk text
|
83
|
+
when '-', :deleted then Card::Diff.render_deleted_chunk text
|
84
|
+
else text
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def truncate_overlap
|
89
|
+
if @remaining_chars < 0
|
90
|
+
if @chunks.last[:action] == :ellipsis
|
91
|
+
@chunks.pop
|
92
|
+
@remaining_chars += @joint.size
|
93
|
+
end
|
94
|
+
|
95
|
+
index = @chunks.size - 1
|
96
|
+
while @remaining_chars < @joint.size && index >= 0
|
97
|
+
if @remaining_chars + @chunks[index][:text].size == @joint.size
|
98
|
+
replace_with_joint index
|
99
|
+
break
|
100
|
+
elsif @remaining_chars + @chunks[index][:text].size > @joint.size
|
101
|
+
cut_with_joint index
|
102
|
+
break
|
103
|
+
else
|
104
|
+
@remaining_chars += @chunks[index][:text].size
|
105
|
+
@chunks.delete_at(index)
|
106
|
+
end
|
107
|
+
index -= 1
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def cut_with_joint index
|
113
|
+
@chunks[index][:text] =
|
114
|
+
@chunks[index][:text][0..(@remaining_chars - @joint.size - 1)]
|
115
|
+
@chunks[index][:text] += @joint
|
116
|
+
end
|
117
|
+
|
118
|
+
def replace_with_joint index
|
119
|
+
@chunks.pop
|
120
|
+
if index - 1 >= 0
|
121
|
+
if @chunks[index - 1][:action] == :added
|
122
|
+
@chunks << { action: :ellipsis, text: @joint }
|
123
|
+
elsif @chunks[index - 1][:action] == :deleted
|
124
|
+
@chunks << { action: :added, text: @joint }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -163,14 +163,21 @@ class Card
|
|
163
163
|
r = Reference.new(key, val, self)
|
164
164
|
refjoin = Join.new(from: self, to: r, to_field: r.infield)
|
165
165
|
joins << refjoin
|
166
|
-
if r.cardquery
|
167
|
-
join_cards r.cardquery, from: refjoin, from_field: r.outfield
|
168
|
-
end
|
166
|
+
restrict_reference r, refjoin if r.cardquery
|
169
167
|
r.conditions.each do |condition|
|
170
168
|
refjoin.conditions << "#{r.table_alias}.#{condition}"
|
171
169
|
end
|
172
170
|
end
|
173
171
|
|
172
|
+
def restrict_reference ref, refjoin
|
173
|
+
val = ref.cardquery
|
174
|
+
if (id = id_from_val(val))
|
175
|
+
add_condition "#{ref.table_alias}.#{ref.outfield} = #{id}"
|
176
|
+
else
|
177
|
+
join_cards val, from: refjoin, from_field: ref.outfield
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
174
181
|
def conjunction val
|
175
182
|
return unless [String, Symbol].member? val.class
|
176
183
|
CONJUNCTIONS[val.to_sym]
|
@@ -183,15 +190,13 @@ class Card
|
|
183
190
|
|
184
191
|
if sort_field == 'count'
|
185
192
|
sort_by_count val, item
|
193
|
+
elsif (join_field = SORT_JOIN_TO_ITEM_MAP[item.to_sym])
|
194
|
+
sq = join_cards(val, to_field: join_field,
|
195
|
+
side: 'LEFT',
|
196
|
+
conditions_on_join: true)
|
197
|
+
@mods[:sort] ||= "#{sq.table_alias}.#{sort_field}"
|
186
198
|
else
|
187
|
-
|
188
|
-
sq = join_cards(val, to_field: join_field,
|
189
|
-
side: 'LEFT',
|
190
|
-
conditions_on_join: true)
|
191
|
-
@mods[:sort] ||= "#{sq.table_alias}.#{sort_field}"
|
192
|
-
else
|
193
|
-
raise BadQuery, "sort item: #{item} not yet implemented"
|
194
|
-
end
|
199
|
+
raise BadQuery, "sort item: #{item} not yet implemented"
|
195
200
|
end
|
196
201
|
end
|
197
202
|
|
@@ -204,7 +209,7 @@ class Card
|
|
204
209
|
group: 'sort_join_field',
|
205
210
|
superquery: self
|
206
211
|
)
|
207
|
-
subselect = Query.new
|
212
|
+
subselect = Query.new val.merge(return: 'id', superquery: self)
|
208
213
|
cs.add_condition "referer_id in (#{subselect.sql})"
|
209
214
|
# FIXME: - SQL generated before SQL phase
|
210
215
|
cs.joins << Join.new(
|
@@ -248,7 +253,8 @@ class Card
|
|
248
253
|
def join_cards val, opts={}
|
249
254
|
conditions_on_join = opts.delete :conditions_on_join
|
250
255
|
s = subquery
|
251
|
-
|
256
|
+
join_opts = { from: self, to: s }.merge opts
|
257
|
+
card_join = Join.new join_opts
|
252
258
|
joins << card_join unless opts[:from].is_a? Join
|
253
259
|
s.conditions_on_join = card_join if conditions_on_join
|
254
260
|
s.interpret val
|