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.
- checksums.yaml +15 -0
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +19 -0
- data/LICENSE.txt +661 -0
- data/README.md +52 -0
- data/Rakefile +30 -0
- data/lib/rticles.rb +8 -0
- data/lib/rticles/document.rb +200 -0
- data/lib/rticles/generators/install_generator.rb +18 -0
- data/lib/rticles/generators/templates/create_documents_and_paragraphs.rb +24 -0
- data/lib/rticles/numbering.rb +56 -0
- data/lib/rticles/paragraph.rb +315 -0
- data/lib/rticles/railtie.rb +5 -0
- data/lib/rticles/version.rb +3 -0
- data/lib/tasks/rticles_tasks.rake +4 -0
- data/rticles.gemspec +26 -0
- data/spec/document_spec.rb +193 -0
- data/spec/dummy/.gitignore +6 -0
- data/spec/dummy/.rspec +1 -0
- data/spec/dummy/README.rdoc +261 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/assets/javascripts/application.js +15 -0
- data/spec/dummy/app/assets/javascripts/documents.js +25 -0
- data/spec/dummy/app/assets/stylesheets/application.css.sass +30 -0
- data/spec/dummy/app/controllers/application_controller.rb +3 -0
- data/spec/dummy/app/controllers/documents_controller.rb +51 -0
- data/spec/dummy/app/controllers/paragraphs_controller.rb +76 -0
- data/spec/dummy/app/helpers/application_helper.rb +2 -0
- data/spec/dummy/app/helpers/documents_helper.rb +2 -0
- data/spec/dummy/app/helpers/paragraphs_helper.rb +2 -0
- data/spec/dummy/app/mailers/.gitkeep +0 -0
- data/spec/dummy/app/models/.gitkeep +0 -0
- data/spec/dummy/app/views/documents/edit.html.haml +16 -0
- data/spec/dummy/app/views/documents/index.html.haml +8 -0
- data/spec/dummy/app/views/documents/new.html.haml +9 -0
- data/spec/dummy/app/views/documents/show.html.haml +7 -0
- data/spec/dummy/app/views/layouts/application.html.haml +13 -0
- data/spec/dummy/app/views/paragraphs/_form.html.haml +8 -0
- data/spec/dummy/app/views/paragraphs/_paragraph.html.haml +21 -0
- data/spec/dummy/app/views/paragraphs/_paragraph_internal.html.haml +18 -0
- data/spec/dummy/app/views/paragraphs/edit.html.haml +7 -0
- data/spec/dummy/app/views/paragraphs/new.html.haml +7 -0
- data/spec/dummy/app/views/paragraphs/update.js.erb +1 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +65 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/inflections.rb +15 -0
- data/spec/dummy/config/initializers/mime_types.rb +5 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +5 -0
- data/spec/dummy/config/routes.rb +14 -0
- data/spec/dummy/db/migrate/20120828203449_create_documents_and_paragraphs.rb +24 -0
- data/spec/dummy/db/schema.rb +35 -0
- data/spec/dummy/lib/assets/.gitkeep +0 -0
- data/spec/dummy/log/.gitkeep +0 -0
- data/spec/dummy/public/404.html +26 -0
- data/spec/dummy/public/422.html +26 -0
- data/spec/dummy/public/500.html +25 -0
- data/spec/dummy/public/favicon.ico +0 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/fixtures/constitution.yml +31 -0
- data/spec/fixtures/ips.yml +484 -0
- data/spec/fixtures/simple.yml +5 -0
- data/spec/paragraph_spec.rb +251 -0
- data/spec/spec_helper.rb +38 -0
- data/spec/support/custom_matchers.rb +1 -0
- data/spec/support/document_macros.rb +30 -0
- 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,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
|