card 1.18.0 → 1.18.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/card.gemspec +20 -16
  4. data/db/migrate_core_cards/20150202143810_import_bootstrap_layout.rb +1 -1
  5. data/db/schema.rb +110 -92
  6. data/lib/card.rb +1 -0
  7. data/lib/card/content.rb +4 -73
  8. data/lib/card/content/chunk.rb +119 -0
  9. data/lib/card/content/parser.rb +75 -0
  10. data/lib/card/diff.rb +25 -398
  11. data/lib/card/diff/lcs.rb +247 -0
  12. data/lib/card/diff/result.rb +131 -0
  13. data/lib/card/director_register.rb +5 -0
  14. data/lib/card/query/attributes.rb +19 -13
  15. data/lib/card/set/event.rb +2 -1
  16. data/lib/card/set_pattern.rb +4 -2
  17. data/lib/card/spec_helper.rb +7 -1
  18. data/lib/card/stage_director.rb +33 -5
  19. data/lib/card/subcards.rb +11 -3
  20. data/lib/card/subdirector_array.rb +14 -1
  21. data/lib/cardio.rb +8 -5
  22. data/mod/01_core/chunk/include.rb +2 -2
  23. data/mod/01_core/chunk/link.rb +3 -3
  24. data/mod/01_core/chunk/literal.rb +20 -14
  25. data/mod/01_core/chunk/query_reference.rb +2 -2
  26. data/mod/01_core/chunk/reference.rb +47 -38
  27. data/mod/01_core/chunk/uri.rb +17 -13
  28. data/mod/01_core/format/html_format.rb +0 -2
  29. data/mod/01_core/set/all/actify.rb +12 -1
  30. data/mod/01_core/set/all/collection.rb +4 -4
  31. data/mod/01_core/set/all/fetch.rb +0 -27
  32. data/mod/01_core/set/all/name.rb +33 -12
  33. data/mod/01_core/set/all/pattern.rb +2 -6
  34. data/mod/01_core/set/all/phases.rb +0 -1
  35. data/mod/01_core/set/all/references.rb +2 -2
  36. data/mod/01_core/set/all/rules.rb +10 -3
  37. data/mod/01_core/set/all/tracked_attributes.rb +0 -1
  38. data/mod/01_core/set/all/type.rb +0 -14
  39. data/mod/01_core/spec/chunk/literal_spec.rb +1 -1
  40. data/mod/01_core/spec/chunk/uri_spec.rb +204 -201
  41. data/mod/01_core/spec/set/all/type_spec.rb +3 -1
  42. data/mod/01_history/lib/card/action.rb +7 -9
  43. data/mod/01_history/set/all/history.rb +6 -1
  44. data/mod/02_basic_types/set/all/all_csv.rb +1 -1
  45. data/mod/02_basic_types/set/type/pointer.rb +20 -9
  46. data/mod/03_machines/lib/javascript/wagn.js.coffee +1 -1
  47. data/mod/04_settings/set/right/structure.rb +7 -1
  48. data/mod/05_email/set/right/follow.rb +22 -22
  49. data/mod/05_email/set/type_plus_right/user/follow.rb +25 -26
  50. data/mod/05_standard/set/all/rich_html/wrapper.rb +12 -6
  51. data/mod/05_standard/set/rstar/rules_editor.rb +6 -4
  52. data/mod/05_standard/set/self/all.rb +0 -10
  53. data/mod/05_standard/set/self/stats.rb +6 -15
  54. data/mod/05_standard/set/type/set.rb +0 -6
  55. data/mod/05_standard/spec/chunk/include_spec.rb +2 -2
  56. data/mod/05_standard/spec/chunk/link_spec.rb +1 -1
  57. data/mod/05_standard/spec/chunk/query_reference_spec.rb +5 -4
  58. data/spec/lib/card/chunk_spec.rb +7 -5
  59. data/spec/lib/card/content_spec.rb +11 -11
  60. data/spec/lib/card/diff_spec.rb +4 -4
  61. data/spec/lib/card/stage_director_spec.rb +56 -0
  62. data/spec/lib/card/subcards_spec.rb +0 -1
  63. data/spec/models/card/type_transition_spec.rb +5 -42
  64. metadata +12 -23
  65. 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
@@ -60,6 +60,11 @@ class Card
60
60
  def delete director
61
61
  return unless @directors
62
62
  @directors.delete director.card
63
+ director.delete
64
+ end
65
+
66
+ def to_s
67
+ directors.values.map(&:to_s).join "\n"
63
68
  end
64
69
  end
65
70
  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
- if (join_field = SORT_JOIN_TO_ITEM_MAP[item.to_sym])
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(val.merge return: 'id', superquery: self)
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
- card_join = Join.new({ from: self, to: s }.merge opts)
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