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.
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