rticles 0.1.0

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 (77) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +1 -0
  4. data/Gemfile +19 -0
  5. data/LICENSE.txt +661 -0
  6. data/README.md +52 -0
  7. data/Rakefile +30 -0
  8. data/lib/rticles.rb +8 -0
  9. data/lib/rticles/document.rb +200 -0
  10. data/lib/rticles/generators/install_generator.rb +18 -0
  11. data/lib/rticles/generators/templates/create_documents_and_paragraphs.rb +24 -0
  12. data/lib/rticles/numbering.rb +56 -0
  13. data/lib/rticles/paragraph.rb +315 -0
  14. data/lib/rticles/railtie.rb +5 -0
  15. data/lib/rticles/version.rb +3 -0
  16. data/lib/tasks/rticles_tasks.rake +4 -0
  17. data/rticles.gemspec +26 -0
  18. data/spec/document_spec.rb +193 -0
  19. data/spec/dummy/.gitignore +6 -0
  20. data/spec/dummy/.rspec +1 -0
  21. data/spec/dummy/README.rdoc +261 -0
  22. data/spec/dummy/Rakefile +7 -0
  23. data/spec/dummy/app/assets/javascripts/application.js +15 -0
  24. data/spec/dummy/app/assets/javascripts/documents.js +25 -0
  25. data/spec/dummy/app/assets/stylesheets/application.css.sass +30 -0
  26. data/spec/dummy/app/controllers/application_controller.rb +3 -0
  27. data/spec/dummy/app/controllers/documents_controller.rb +51 -0
  28. data/spec/dummy/app/controllers/paragraphs_controller.rb +76 -0
  29. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  30. data/spec/dummy/app/helpers/documents_helper.rb +2 -0
  31. data/spec/dummy/app/helpers/paragraphs_helper.rb +2 -0
  32. data/spec/dummy/app/mailers/.gitkeep +0 -0
  33. data/spec/dummy/app/models/.gitkeep +0 -0
  34. data/spec/dummy/app/views/documents/edit.html.haml +16 -0
  35. data/spec/dummy/app/views/documents/index.html.haml +8 -0
  36. data/spec/dummy/app/views/documents/new.html.haml +9 -0
  37. data/spec/dummy/app/views/documents/show.html.haml +7 -0
  38. data/spec/dummy/app/views/layouts/application.html.haml +13 -0
  39. data/spec/dummy/app/views/paragraphs/_form.html.haml +8 -0
  40. data/spec/dummy/app/views/paragraphs/_paragraph.html.haml +21 -0
  41. data/spec/dummy/app/views/paragraphs/_paragraph_internal.html.haml +18 -0
  42. data/spec/dummy/app/views/paragraphs/edit.html.haml +7 -0
  43. data/spec/dummy/app/views/paragraphs/new.html.haml +7 -0
  44. data/spec/dummy/app/views/paragraphs/update.js.erb +1 -0
  45. data/spec/dummy/config.ru +4 -0
  46. data/spec/dummy/config/application.rb +65 -0
  47. data/spec/dummy/config/boot.rb +10 -0
  48. data/spec/dummy/config/database.yml +25 -0
  49. data/spec/dummy/config/environment.rb +5 -0
  50. data/spec/dummy/config/environments/development.rb +37 -0
  51. data/spec/dummy/config/environments/production.rb +67 -0
  52. data/spec/dummy/config/environments/test.rb +37 -0
  53. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  54. data/spec/dummy/config/initializers/inflections.rb +15 -0
  55. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  56. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  57. data/spec/dummy/config/initializers/session_store.rb +8 -0
  58. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  59. data/spec/dummy/config/locales/en.yml +5 -0
  60. data/spec/dummy/config/routes.rb +14 -0
  61. data/spec/dummy/db/migrate/20120828203449_create_documents_and_paragraphs.rb +24 -0
  62. data/spec/dummy/db/schema.rb +35 -0
  63. data/spec/dummy/lib/assets/.gitkeep +0 -0
  64. data/spec/dummy/log/.gitkeep +0 -0
  65. data/spec/dummy/public/404.html +26 -0
  66. data/spec/dummy/public/422.html +26 -0
  67. data/spec/dummy/public/500.html +25 -0
  68. data/spec/dummy/public/favicon.ico +0 -0
  69. data/spec/dummy/script/rails +6 -0
  70. data/spec/fixtures/constitution.yml +31 -0
  71. data/spec/fixtures/ips.yml +484 -0
  72. data/spec/fixtures/simple.yml +5 -0
  73. data/spec/paragraph_spec.rb +251 -0
  74. data/spec/spec_helper.rb +38 -0
  75. data/spec/support/custom_matchers.rb +1 -0
  76. data/spec/support/document_macros.rb +30 -0
  77. metadata +220 -0
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ Rticles
2
+ =======
3
+
4
+ Rticles is a Rails plugin that allows for web-based editing of legal documents. It lets you create nested, numbered paragraphs, along with intra-document references that remain accurate as paragraphs are inserted, removed and moved.
5
+
6
+ This is a technology test for [One Click Orgs](http://github.com/oneclickorgs/one-click-orgs/).
7
+
8
+ License
9
+ -------
10
+
11
+ Rticles is licensed under the [GNU Affero General Public License, Version 3](http://www.fsf.org/licensing/licenses/agpl-3.0.html).
12
+
13
+ Rticles
14
+ Copyright (C) 2011, 2012 Circus Foundation
15
+
16
+ This program is free software: you can redistribute it and/or modify
17
+ it under the terms of the GNU Affero General Public License as
18
+ published by the Free Software Foundation, either version 3 of the
19
+ License, or (at your option) any later version.
20
+
21
+ This program is distributed in the hope that it will be useful,
22
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
23
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
24
+ GNU Affero General Public License for more details.
25
+
26
+ You should have received a copy of the GNU Affero General Public License
27
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
28
+
29
+ Rticles incorporates work covered by the following copyright and permission notice:
30
+
31
+ activerecord
32
+
33
+ Copyright (c) 2004-2011 David Heinemeier Hansson
34
+
35
+ Permission is hereby granted, free of charge, to any person obtaining
36
+ a copy of this software and associated documentation files (the
37
+ "Software"), to deal in the Software without restriction, including
38
+ without limitation the rights to use, copy, modify, merge, publish,
39
+ distribute, sublicense, and/or sell copies of the Software, and to
40
+ permit persons to whom the Software is furnished to do so, subject to
41
+ the following conditions:
42
+
43
+ The above copyright notice and this permission notice shall be
44
+ included in all copies or substantial portions of the Software.
45
+
46
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
47
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
48
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
49
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
50
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
51
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
52
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+
8
+ begin
9
+ require 'rdoc/task'
10
+ rescue LoadError
11
+ require 'rdoc/rdoc'
12
+ require 'rake/rdoctask'
13
+ RDoc::Task = Rake::RDocTask
14
+ end
15
+
16
+ require 'rspec/core/rake_task'
17
+
18
+ RSpec::Core::RakeTask.new(:spec)
19
+
20
+ RDoc::Task.new(:rdoc) do |rdoc|
21
+ rdoc.rdoc_dir = 'rdoc'
22
+ rdoc.title = 'Rticles'
23
+ rdoc.options << '--line-numbers'
24
+ rdoc.rdoc_files.include('README.rdoc')
25
+ rdoc.rdoc_files.include('lib/**/*.rb')
26
+ end
27
+
28
+ task :default => :spec
29
+
30
+ Bundler::GemHelper.install_tasks
data/lib/rticles.rb ADDED
@@ -0,0 +1,8 @@
1
+ module Rticles
2
+ end
3
+
4
+ require 'rticles/version'
5
+ require 'rticles/numbering'
6
+ require 'rticles/document'
7
+ require 'rticles/paragraph'
8
+ require 'rticles/railtie'
@@ -0,0 +1,200 @@
1
+ # encoding: UTF-8
2
+
3
+ require 'yaml'
4
+
5
+ module Rticles
6
+ class Document < ActiveRecord::Base
7
+ NAME_RE = /\A#rticles#name#([A-Za-z_]+) /
8
+ TOPIC_RE = /\A#rticles#topic#([A-Za-z_]+) /
9
+ CONTINUATION_RE = /\A#rticles#continue /
10
+ HEADING_RE = /\A#rticles#heading(#\d+|) /
11
+
12
+ has_many :paragraphs, :order => 'position'
13
+ has_many :top_level_paragraphs, :class_name => 'Paragraph', :order => 'position', :conditions => "parent_id IS NULL"
14
+
15
+ alias_method :children, :paragraphs
16
+
17
+ attr_accessor :insertions, :choices
18
+
19
+ after_initialize :after_initialize
20
+ def after_initialize
21
+ set_up_insertions
22
+ set_up_choices
23
+ end
24
+
25
+ def set_up_insertions
26
+ self.insertions ||= {}
27
+ self.insertions = insertions.with_indifferent_access
28
+ end
29
+
30
+ def set_up_choices
31
+ self.choices ||= {}
32
+ self.choices = choices.with_indifferent_access
33
+ end
34
+
35
+ def outline(options={})
36
+ options = options.with_indifferent_access
37
+ for_display = options[:for_display]
38
+
39
+ o = []
40
+ top_level_paragraphs.each do |tlp|
41
+ body = for_display ? tlp.body_for_display({:insertions => insertions, :choices => choices}.merge(options)) : tlp.body
42
+ if body
43
+ o.push(for_display ? tlp.body_for_display({:insertions => insertions, :choices => choices}.merge(options)) : tlp.body)
44
+ unless tlp.children.empty?
45
+ o.push(sub_outline(tlp, options))
46
+ end
47
+ end
48
+ end
49
+ o
50
+ end
51
+
52
+ def to_html
53
+ html = "<section>"
54
+ html += Rticles::Paragraph.generate_html(top_level_paragraphs,
55
+ :insertions => insertions,
56
+ :choices => choices,
57
+ :numbering_config => numbering_config
58
+ )
59
+ html += "</section>"
60
+ html.html_safe
61
+ end
62
+
63
+ def to_yaml
64
+ outline.to_yaml
65
+ end
66
+
67
+ def self.from_yaml(yaml)
68
+ parsed_yaml = YAML.load(yaml)
69
+ document = self.create
70
+
71
+ create_paragraphs_from_array(document, nil, parsed_yaml)
72
+
73
+ document
74
+ end
75
+
76
+ def self.create_paragraphs_from_array(document, parent, array)
77
+ array.each do |text_or_sub_array|
78
+ case text_or_sub_array
79
+ when String
80
+ name = nil
81
+ topic = nil
82
+ continuation = false
83
+ heading = nil
84
+
85
+ if name_match = text_or_sub_array.match(NAME_RE)
86
+ text_or_sub_array = text_or_sub_array.sub(NAME_RE, '')
87
+ name = name_match[1]
88
+ end
89
+
90
+ if topic_match = text_or_sub_array.match(TOPIC_RE)
91
+ text_or_sub_array = text_or_sub_array.sub(TOPIC_RE, '')
92
+ topic = topic_match[1]
93
+ end
94
+
95
+ if text_or_sub_array.match(CONTINUATION_RE)
96
+ text_or_sub_array = text_or_sub_array.sub(CONTINUATION_RE, '')
97
+ continuation = true
98
+ end
99
+
100
+ if heading_match = text_or_sub_array.match(HEADING_RE)
101
+ text_or_sub_array = text_or_sub_array.sub(HEADING_RE, '')
102
+ if heading_match[1].empty?
103
+ heading = 1
104
+ else
105
+ heading = heading_match[1].sub(/\A#/, '').to_i
106
+ end
107
+ end
108
+ document.paragraphs.create(
109
+ :parent_id => parent ? parent.id : nil,
110
+ :body => text_or_sub_array,
111
+ :name => name,
112
+ :topic => topic,
113
+ :heading => heading,
114
+ :continuation => continuation
115
+ )
116
+ when Array
117
+ paragraphs_relation = parent ? parent.children : document.paragraphs.select{|p| p.parent_id.nil?}
118
+ if paragraphs_relation.empty?
119
+ raise RuntimeError, "jump in nesting at: #{text_or_sub_array.first}"
120
+ end
121
+ create_paragraphs_from_array(
122
+ document,
123
+ paragraphs_relation.last,
124
+ text_or_sub_array
125
+ )
126
+ end
127
+ end
128
+ end
129
+
130
+ def paragraph_for_reference(raw_reference)
131
+ # TODO optimise
132
+ Rails.logger.debug("Finding raw reference: #{raw_reference}")
133
+ paragraphs.all.detect{|p| p.full_index == raw_reference}
134
+ end
135
+
136
+ def paragraph_numbers_for_topic(topic, consolidate=false)
137
+ relevant_paragraphs = paragraphs.where(:topic => topic)
138
+ relevant_paragraphs = relevant_paragraphs.for_choices(choices)
139
+ paragraph_numbers = relevant_paragraphs.map{|p| p.full_index(true, choices)}.select{|i| !i.nil?}.sort
140
+
141
+ if consolidate
142
+ consolidate_paragraph_numbers(paragraph_numbers)
143
+ else
144
+ paragraph_numbers.join(', ')
145
+ end
146
+ end
147
+
148
+ def numbering_config
149
+ @numbering_config ||= Rticles::Numbering::Config.new
150
+ end
151
+
152
+ protected
153
+
154
+ def consolidate_paragraph_numbers(numbers)
155
+ numbers = numbers.sort
156
+ consolidated_numbers = []
157
+ current_run = []
158
+ numbers.each do |n|
159
+ if current_run.empty? || is_adjacent?(current_run.last, n)
160
+ current_run.push(n)
161
+ else
162
+ if current_run.length == 1
163
+ consolidated_numbers.push(current_run[0])
164
+ else
165
+ consolidated_numbers.push("#{current_run[0]}–#{current_run[-1]}")
166
+ end
167
+ current_run = [n]
168
+ end
169
+ end
170
+ if current_run.length == 1
171
+ consolidated_numbers.push(current_run[0])
172
+ else
173
+ consolidated_numbers.push("#{current_run[0]}–#{current_run[-1]}")
174
+ end
175
+ consolidated_numbers.join(', ')
176
+ end
177
+
178
+ def is_adjacent?(a, b)
179
+ # TODO Make this smart enough to handle sub-numbers like '2.4', '2.6'
180
+ b.to_i == a.to_i + 1
181
+ end
182
+
183
+ def sub_outline(p, options={})
184
+ options = options.with_indifferent_access
185
+ for_display = options[:for_display]
186
+
187
+ o = []
188
+ p.children.each do |c|
189
+ body = for_display ? c.body_for_display({:insertions => insertions, :choices => choices}.merge(options)) : c.body
190
+ if body
191
+ o.push(body)
192
+ unless c.children.empty?
193
+ o.push(sub_outline(c, options))
194
+ end
195
+ end
196
+ end
197
+ o
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,18 @@
1
+ module Rticles
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ include Rails::Generators::Migration
5
+
6
+ source_root File.expand_path('../templates', __FILE__)
7
+
8
+ def self.next_migration_number(dirname)
9
+ next_migration_number = current_migration_number(dirname) + 1
10
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
11
+ end
12
+
13
+ def create_migration
14
+ migration_template 'create_documents_and_paragraphs.rb', 'db/migrate/create_documents_and_paragraphs.rb'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,24 @@
1
+ class CreateDocumentsAndParagraphs < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :documents do |t|
4
+ t.string 'title'
5
+ t.timestamps
6
+ end
7
+ create_table :paragraphs do |t|
8
+ t.text 'body'
9
+ t.integer 'position'
10
+ t.integer 'parent_id'
11
+ t.integer 'document_id'
12
+ t.integer 'heading'
13
+ t.boolean 'continuation'
14
+ t.string 'name'
15
+ t.string 'topic'
16
+ t.timestamps
17
+ end
18
+ end
19
+
20
+ def self.down
21
+ drop_table :paragraphs
22
+ drop_table :documents
23
+ end
24
+ end
@@ -0,0 +1,56 @@
1
+ require 'roman-numerals'
2
+
3
+ module Rticles
4
+ module Numbering
5
+ DECIMAL = :decimal
6
+ LOWER_ALPHA = :lower_alpha
7
+ LOWER_ROMAN = :lower_roman
8
+
9
+ def self.number_to_string(number, style)
10
+ case style
11
+ when DECIMAL
12
+ number.to_s
13
+ when LOWER_ALPHA
14
+ number_to_alpha(number)
15
+ when LOWER_ROMAN
16
+ RomanNumerals.to_roman(number).downcase
17
+ end
18
+ end
19
+
20
+ class Config
21
+ attr_accessor :separator, :innermost_only
22
+
23
+ def initialize
24
+ self.separator = '.'
25
+ @level_configs = []
26
+ end
27
+
28
+ def [](level)
29
+ @level_configs[level] ||= LevelConfig.new
30
+ end
31
+
32
+ class LevelConfig
33
+ attr_accessor :style, :format
34
+
35
+ def initialize
36
+ self.style = Rticles::Numbering::DECIMAL
37
+ self.format = '#'
38
+ end
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ def self.number_to_alpha(number)
45
+ numerator = number
46
+ result = ''
47
+ while numerator > 0 do
48
+ modulo = (numerator - 1) % 26
49
+ result = (97 + modulo).chr + result
50
+ numerator = (numerator - modulo) / 26
51
+ end
52
+ result
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,315 @@
1
+ require 'acts_as_list'
2
+
3
+ module Rticles
4
+ class Paragraph < ActiveRecord::Base
5
+ attr_accessible :body, :parent_id, :after_id, :position, :before_id, :heading, :continuation,
6
+ :name, :topic
7
+
8
+ belongs_to :document
9
+ belongs_to :parent, :class_name => 'Paragraph'
10
+ has_many :children, :class_name => 'Paragraph', :foreign_key => 'parent_id', :order => 'position', :dependent => :destroy
11
+
12
+ acts_as_list :scope => [:document_id, :parent_id]
13
+
14
+ scope :for_choices, lambda {|choices|
15
+ choices_condition = ["", {}]
16
+ choices.each do |k, v|
17
+ choices_condition[0] += "AND body NOT LIKE :#{k}"
18
+ choices_condition[1][k.to_sym] = "#rticles##{v ? 'false' : 'true'}##{k}%"
19
+ end
20
+ choices_condition[0].sub!(/\AAND /, '')
21
+ where(choices_condition)
22
+ }
23
+
24
+ before_create :set_document_id
25
+ def set_document_id
26
+ if parent
27
+ self.document_id ||= parent.document_id
28
+ end
29
+ end
30
+
31
+ attr_accessor :before_id, :after_id
32
+
33
+ after_create :set_parent_and_position
34
+ def set_parent_and_position
35
+ if before_id.present?
36
+ sibling = self.class.find(before_id)
37
+ self.update_attribute(:parent_id, sibling.parent_id)
38
+ insert_at(sibling.position)
39
+ elsif after_id.present?
40
+ sibling = self.class.find(after_id)
41
+ self.update_attribute(:parent_id, sibling.parent_id)
42
+ insert_at(self.class.find(after_id).position + 1)
43
+ end
44
+ end
45
+
46
+ def heading?
47
+ heading && heading > 0
48
+ end
49
+
50
+ def heading_level
51
+ ancestors.length + (heading ? heading : 0)
52
+ end
53
+
54
+ def level
55
+ ancestors.length + 1
56
+ end
57
+
58
+ def index(choices=nil)
59
+ return nil if heading? || continuation?
60
+
61
+ predecessors = higher_items.where(['(heading = 0 OR heading IS NULL) AND (continuation = ? OR continuation IS NULL)', false])
62
+
63
+ if choices.present?
64
+ predecessors = predecessors.for_choices(choices)
65
+ end
66
+
67
+ predecessors.count + 1
68
+ end
69
+
70
+ def full_index(recalculate=false, choices=nil, numbering_config=nil)
71
+ return nil if heading? || continuation?
72
+
73
+ return @full_index if @full_index && !recalculate
74
+
75
+ if numbering_config.nil?
76
+ numbering_config = Rticles::Numbering::Config.new
77
+ end
78
+
79
+ if numbering_config.innermost_only
80
+ @full_index = numbering_config[level].format.sub('#', Rticles::Numbering.number_to_string(index(choices), numbering_config[level].style))
81
+ else
82
+ @full_index = ancestors.unshift(self).reverse.map do |p|
83
+ numbering_config[p.level].format.sub('#', Rticles::Numbering.number_to_string(p.index(choices), numbering_config[p.level].style))
84
+ end
85
+ @full_index = @full_index.join(numbering_config.separator)
86
+ end
87
+ end
88
+
89
+ def ancestors
90
+ node = self
91
+ nodes = []
92
+ nodes.push(node = node.parent) while node.parent
93
+ nodes
94
+ end
95
+
96
+ def can_move_lower?
97
+ !!lower_item
98
+ end
99
+
100
+ def can_move_higher?
101
+ !!higher_item
102
+ end
103
+
104
+ def can_indent?
105
+ !!higher_item
106
+ end
107
+
108
+ def indent!
109
+ return unless can_indent?
110
+ new_parent_id = higher_item.id
111
+ remove_from_list
112
+ update_attribute(:parent_id, new_parent_id)
113
+ send(:assume_bottom_position)
114
+ end
115
+
116
+ def can_outdent?
117
+ !!parent_id
118
+ end
119
+
120
+ def outdent!
121
+ return unless can_outdent?
122
+ new_parent_id = parent.parent_id
123
+ new_position = parent.position + 1
124
+ reparent_lower_items_under_self
125
+ remove_from_list
126
+ update_attribute(:parent_id, new_parent_id)
127
+ insert_at(new_position)
128
+ end
129
+
130
+ def higher_items
131
+ return nil unless in_list?
132
+ acts_as_list_class.where(
133
+ "#{scope_condition} AND #{position_column} < #{(send(position_column).to_i).to_s}"
134
+ )
135
+ end
136
+
137
+ def lower_items
138
+ return nil unless in_list?
139
+ acts_as_list_class.where(
140
+ "#{scope_condition} AND #{position_column} > #{(send(position_column).to_i).to_s}"
141
+ )
142
+ end
143
+
144
+ def reparent_lower_items_under_self
145
+ return unless in_list?
146
+ acts_as_list_class.update_all(
147
+ "#{position_column} = (#{position_column} - #{position}), parent_id = #{id}", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
148
+ )
149
+ end
150
+
151
+ before_save :normalise_references
152
+ def normalise_references
153
+ return if body.blank?
154
+ raw_reference_re = /!(\d\.)*\d/
155
+ Rails.logger.debug("Body: #{body}")
156
+ self.body = body.gsub(raw_reference_re) do |match|
157
+ raw_reference = match.sub('!', '')
158
+ '#rticles#' + document.paragraph_for_reference(raw_reference).id.to_s
159
+ end
160
+ end
161
+
162
+ def body_for_display(options={})
163
+ options = options.with_indifferent_access
164
+
165
+ if options[:insertions]
166
+ @insertions = options[:insertions]
167
+ end
168
+
169
+ if options[:choices]
170
+ @choices = options[:choices]
171
+ end
172
+
173
+ with_meta_characters = options[:with_meta_characters] || false
174
+
175
+ result = resolve_choices(body)
176
+ return result if result.nil?
177
+
178
+ result = resolve_references(result, with_meta_characters)
179
+ result = resolve_insertions(result)
180
+
181
+ if options[:with_index] && full_index(true, choices, options[:numbering_config])
182
+ result = "#{full_index} #{result}"
183
+ end
184
+
185
+ result
186
+ end
187
+
188
+ def body_with_resolved_references(with_meta_characters=false)
189
+ resolve_references(body, with_meta_characters)
190
+ end
191
+
192
+ def resolve_references(string, with_meta_characters=false)
193
+ return string if string.blank?
194
+ normalised_reference_re = /#rticles#(\d+)/
195
+ string.gsub(normalised_reference_re) do |match|
196
+ normalised_reference = match.sub('#rticles#', '')
197
+ result = with_meta_characters ? '!' : ''
198
+ result += document.paragraphs.find(normalised_reference).full_index
199
+ result
200
+ end
201
+ end
202
+
203
+ def resolve_insertions(string)
204
+ return string if string.blank?
205
+ insertion_re = /#rticles#([A-Za-z_]+)/
206
+ string.gsub(insertion_re) do |match|
207
+ insertion_name = match.sub('#rticles#', '')
208
+ if insertions[insertion_name].present?
209
+ insertions[insertion_name]
210
+ else
211
+ "[#{insertion_name.humanize.upcase}]"
212
+ end
213
+ end
214
+ end
215
+
216
+ def resolve_choices(string)
217
+ choice_re = /\A#rticles#(true|false)#([A-Za-z_]+) /
218
+ match = string.match(choice_re)
219
+ return string if !match
220
+
221
+ choice_name = match[2]
222
+ choice_parameter = match[1]
223
+
224
+ if (choices[choice_name] && choice_parameter == 'true') || (!choices[choice_name] && choice_parameter == 'false')
225
+ string.sub(choice_re, '')
226
+ else
227
+ nil
228
+ end
229
+ end
230
+
231
+ def prepare_for_editing
232
+ self.body = body_with_resolved_references(true)
233
+ self
234
+ end
235
+
236
+ def self.generate_html(paragraphs, options={})
237
+ paragraph_groups = []
238
+ paragraphs.each do |paragraph|
239
+ if paragraph.continuation?
240
+ paragraph_groups.last.push(paragraph)
241
+ else
242
+ paragraph_groups.push([paragraph])
243
+ end
244
+ end
245
+ generate_html_for_paragraph_groups(paragraph_groups, options)
246
+ end
247
+
248
+ protected
249
+
250
+ def self.generate_html_for_paragraph_groups(paragraph_groups, options={})
251
+ previous_type = nil
252
+ html = paragraph_groups.inject("") do |memo, paragraph_group|
253
+ # FIXME: Don't generate HTML by interpolating into a string;
254
+ # use some standard library function that provides some safe
255
+ # escaping defaults, etc..
256
+ if paragraph_group.first.heading?
257
+ if previous_type == :paragraph
258
+ memo += "</ol>"
259
+ end
260
+ if paragraph_group.length == 1
261
+ memo += generate_html_for_paragraphs(paragraph_group, options)
262
+ else
263
+ memo += "<hgroup>#{generate_html_for_paragraphs(paragraph_group, options)}</hgroup>"
264
+ end
265
+ previous_type = :heading
266
+ else
267
+ unless previous_type == :paragraph
268
+ memo += "<ol>"
269
+ end
270
+ memo += "<li>#{generate_html_for_paragraphs(paragraph_group, options)}</li>"
271
+ previous_type = :paragraph
272
+ end
273
+ memo
274
+ end
275
+ if previous_type == :paragraph
276
+ html += "</ol>"
277
+ end
278
+ end
279
+
280
+ def self.generate_html_for_paragraphs(paragraphs, options={})
281
+ paragraphs.inject("") do |memo, paragraph|
282
+ body = paragraph.body_for_display({:with_index => true}.merge(options))
283
+ return memo if body.nil?
284
+
285
+ if paragraph.heading?
286
+ memo += "<h#{paragraph.heading_level}>#{body}</h#{paragraph.heading_level}>"
287
+ else
288
+ memo += body
289
+ end
290
+
291
+ if !paragraph.children.empty?
292
+ memo += generate_html(paragraph.children, options)
293
+ end
294
+ memo
295
+ end
296
+ end
297
+
298
+ def insertions
299
+ return @insertions.with_indifferent_access if @insertions
300
+ begin
301
+ (parent || document).insertions.with_indifferent_access
302
+ rescue NoMethodError
303
+ raise RuntimeError, "parent was nil when finding insertions; I am: #{self.inspect}"
304
+ end
305
+ end
306
+
307
+ def choices
308
+ if @choices
309
+ @choices.with_indifferent_access
310
+ else
311
+ {}.with_indifferent_access
312
+ end
313
+ end
314
+ end
315
+ end