card 1.18.0 → 1.18.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.
- 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
|