propolize 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.
- data/LICENSE.txt +674 -0
- data/Rakefile +32 -0
- data/lib/propolize.rb +963 -0
- metadata +49 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
begin
|
|
2
|
+
require "bundler"
|
|
3
|
+
Bundler.setup
|
|
4
|
+
rescue LoadError
|
|
5
|
+
$stderr.puts "You need to have Bundler installed to be able build this gem."
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
gemspec = eval(File.read("propolize.gemspec"))
|
|
9
|
+
|
|
10
|
+
desc "Validate the gemspec"
|
|
11
|
+
task :gemspec do
|
|
12
|
+
gemspec.validate
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
desc "Build gem locally"
|
|
16
|
+
task :build => :gemspec do
|
|
17
|
+
system "gem build #{gemspec.name}.gemspec"
|
|
18
|
+
FileUtils.mkdir_p "pkg"
|
|
19
|
+
FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc "Install gem locally"
|
|
23
|
+
task :install => :build do
|
|
24
|
+
system "gem install pkg/#{gemspec.name}-#{gemspec.version}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
desc "Clean automatically generated files"
|
|
28
|
+
task :clean do
|
|
29
|
+
FileUtils.rm_rf "pkg"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
task :default => :build
|
data/lib/propolize.rb
ADDED
|
@@ -0,0 +1,963 @@
|
|
|
1
|
+
require 'erb'
|
|
2
|
+
|
|
3
|
+
module Propolize
|
|
4
|
+
|
|
5
|
+
# Propolize defines a very specific and constrained document structure which supports "propositional writing"
|
|
6
|
+
# It is similiar to Markdown, but with a limited set of Markdown features, and with some extensions
|
|
7
|
+
# particular to this document structure.
|
|
8
|
+
#
|
|
9
|
+
# Propolize source code consists of the following types of 'chunk', where each chunk is one or more lines:
|
|
10
|
+
#
|
|
11
|
+
# 1. A special property definition or command (starting with '##' at the beginning of the line)
|
|
12
|
+
# 2. A list (which will map to an HTML list) which starts with '* ' at the beginning of the line for each list item.
|
|
13
|
+
# (The end of the list is marked by a blank line.)
|
|
14
|
+
# 3. A 'proposition' (a special type of heading), which starts with '# ' at the beginning of the line
|
|
15
|
+
# 4. A secondary heading - a line with a following line containing only '---------' characters
|
|
16
|
+
# 5. A paragraph - starting with a line which is not any of the above
|
|
17
|
+
#
|
|
18
|
+
# (Note: secondary headings are only allowed in the appendix, because if they were in either the introduction
|
|
19
|
+
# or the propositions, this would distract from the main idea that propositions in a propositional document
|
|
20
|
+
# _are_ the headings. The appendix can be thought of as additional explanatory material that does not fit into
|
|
21
|
+
# the main propositional format.)
|
|
22
|
+
#
|
|
23
|
+
# Special property definitions and commands are terminated by a following blank line or end of file _or_ by the start
|
|
24
|
+
# of another special property definition.
|
|
25
|
+
# All other chunks are terminated by a following blank line or the end of the file.
|
|
26
|
+
#
|
|
27
|
+
# A propositional document also has a higher level structure in that it consists of an introduction followed
|
|
28
|
+
# by a sequence of propositions-with-explanations, followed by an optional appendix.
|
|
29
|
+
#
|
|
30
|
+
# The introduction consists only of lists or paragraphs (i.e. no secondary headings)
|
|
31
|
+
#
|
|
32
|
+
# Each proposition can be followed by zero or more lists of paragraphs (there are no secondary headings)
|
|
33
|
+
#
|
|
34
|
+
# The appendix consists of a sequence of secondary headings, lists or paragraphs.
|
|
35
|
+
# The appendix is started by a special '##appendix' command
|
|
36
|
+
#
|
|
37
|
+
# Special property tags can occur anywhere. They are of the form
|
|
38
|
+
# ##date 23 May, 2014
|
|
39
|
+
#
|
|
40
|
+
# (which defines the 'date' property to be '23 May, 2014')
|
|
41
|
+
#
|
|
42
|
+
# Required properties are 'date', 'author' and 'title'.
|
|
43
|
+
# (Note: properties can be passed in via the 'properties' argument of the 'propolize' method,
|
|
44
|
+
# in which case they do not need to appear in the source code.)
|
|
45
|
+
#
|
|
46
|
+
# Detailed markup occurs within 'text' sections, these occur in the following contexts:
|
|
47
|
+
# * Propositions
|
|
48
|
+
# * Paragraphs
|
|
49
|
+
# * List items
|
|
50
|
+
# * Secondary headings
|
|
51
|
+
# * Text inside link definitions (see below)
|
|
52
|
+
#
|
|
53
|
+
# The detailed markup includes the following:
|
|
54
|
+
#
|
|
55
|
+
# * '*' to delineate italic text
|
|
56
|
+
# * '**' to delineate bold text
|
|
57
|
+
# * '[]' for anchor targets for the form '[name:]' for normal anchors, and '[name::]' for numbered footnote anchors
|
|
58
|
+
# * '[]()' for links as in '[http://example.com/](An example website)'. Three types of URL definition exist -
|
|
59
|
+
# * [name:] for normal anchors
|
|
60
|
+
# * [name::] for numbered footnote anchors
|
|
61
|
+
# * [url] for all other URL's
|
|
62
|
+
#
|
|
63
|
+
# There is also a post-processing step where '--' is converted into '–'
|
|
64
|
+
#
|
|
65
|
+
# Two other special items parsed are:
|
|
66
|
+
# * '\' followed by a character will output the HTML-escaped version of that character
|
|
67
|
+
# * HTML entities (e.g. '–') are output as is
|
|
68
|
+
#
|
|
69
|
+
# All other text is output as HTML-escaped text.
|
|
70
|
+
#
|
|
71
|
+
# There are also special qualifier prefixes:
|
|
72
|
+
#
|
|
73
|
+
# 1. The special tag qualifier may occur at the beginning of a paragraph, in the form '#:<tag> ',
|
|
74
|
+
# where <tag> is a special tag (currently the only option is "bq" for "blockquote").
|
|
75
|
+
#
|
|
76
|
+
# 2. The 'critique' qualifier '?? ' can occur at the beginning of a paragraph or a list, and it qualifies
|
|
77
|
+
# the list or paragraph as being part of the 'critique' where a propositional document
|
|
78
|
+
# is being written as a critique of some other propositional document.
|
|
79
|
+
#
|
|
80
|
+
|
|
81
|
+
module Helpers
|
|
82
|
+
def html_escape(s)
|
|
83
|
+
s.to_s.gsub(/&/, "&").gsub(/>/, ">").gsub(/</, "<")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
alias h html_escape
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# A very simple string buffer that maintains data as an array of strings
|
|
90
|
+
# and joins them all together when the final result is required.
|
|
91
|
+
class StringBuffer
|
|
92
|
+
def initialize
|
|
93
|
+
@strings = []
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def write(string)
|
|
97
|
+
@strings.push(string)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def to_string
|
|
101
|
+
return @strings.join("")
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# A proposition with an explanation. The proposition is effectively the headline,
|
|
106
|
+
# and the explanation is a sequence of "explanation items" (i.e. paragraphs or lists).
|
|
107
|
+
class PropositionWithExplanation
|
|
108
|
+
# Create with initial proposition (and empty list of explanation items)
|
|
109
|
+
def initialize(proposition)
|
|
110
|
+
@proposition = proposition
|
|
111
|
+
@explanationItems = []
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Add one explanation item to the list of explanation items
|
|
115
|
+
def addExplanationItem(item)
|
|
116
|
+
@explanationItems.push(item)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Dump to stdout (for debugging/tracing)
|
|
120
|
+
def dump(indent)
|
|
121
|
+
puts ("#{indent}#{@proposition}")
|
|
122
|
+
for item in @explanationItems do
|
|
123
|
+
puts ("#{indent} #{item}")
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Output as HTML (Each proposition is one item in a list of propositions, so it is output as a <li> item.)
|
|
128
|
+
def toHtml
|
|
129
|
+
return "<li>\n#{@proposition.toHtml}\n#{@explanationItems.map(&:toHtml).join("\n")}\n</li>"
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# A section of source text being processed as part of a document
|
|
135
|
+
class TextBeingProcessed
|
|
136
|
+
|
|
137
|
+
include Helpers
|
|
138
|
+
|
|
139
|
+
# Parsers in order of priority - each one is a pair consisting of:
|
|
140
|
+
# 1. regex to greedily match as much as possible of the text being parsed, and
|
|
141
|
+
# 2. the name of the processing method to call, passing in the match values
|
|
142
|
+
|
|
143
|
+
# Plain text, any text not containing '\', '*', '[' or '&'
|
|
144
|
+
@@plainTextParser = [/\A[^\\\*\[&]+/m, :processPlainText]
|
|
145
|
+
|
|
146
|
+
# Backslash item, '\' followed by the quoted character
|
|
147
|
+
@@backslashParser = [/\A\\(.)/m, :processBackslash]
|
|
148
|
+
|
|
149
|
+
# An HTML entity, starts with '&', then an alphanumerical identifier, or, '#' + a number, followed by ';'
|
|
150
|
+
@@entityParser = [/\A&(([A-Za-z0-9]+)|(#[0-9]+));/m, :processEntity]
|
|
151
|
+
|
|
152
|
+
# A pair of asterisks
|
|
153
|
+
@@doubleAsterixParser = [/\A\*\*/m, :processDoubleAsterix]
|
|
154
|
+
|
|
155
|
+
# A single asterisk
|
|
156
|
+
@@singleAsterixParser = [/\A\*/m, :processSingleAsterix]
|
|
157
|
+
|
|
158
|
+
# text enclosed by '[' and ']', with an optional following section enclosed by '(' and ')'
|
|
159
|
+
@@linkOrAnchorParser = [/\A\[([^\]]*)\](\(([^\)]+)\)|)/m, :processLinkOrAnchor]
|
|
160
|
+
|
|
161
|
+
# Parsers to be applied inside link text (everything _except_ the link/anchor parser)
|
|
162
|
+
@@linkTextParsers = [@@plainTextParser, @@backslashParser, @@entityParser,
|
|
163
|
+
@@doubleAsterixParser, @@singleAsterixParser]
|
|
164
|
+
|
|
165
|
+
# Parsers to be applied outside link text
|
|
166
|
+
@@fullTextParsers = @@linkTextParsers + [@@linkOrAnchorParser]
|
|
167
|
+
|
|
168
|
+
# Initialise -
|
|
169
|
+
# document - source document
|
|
170
|
+
# text - the actual text string
|
|
171
|
+
# writer - to which the output is written
|
|
172
|
+
# weAreInsideALink - are we inside a link? (if so, don't attempt to parse any inner links)
|
|
173
|
+
def initialize(document, text, writer, weAreInsideALink)
|
|
174
|
+
@document = document
|
|
175
|
+
@text = text
|
|
176
|
+
@writer = writer
|
|
177
|
+
@pos = 0
|
|
178
|
+
@italic = false
|
|
179
|
+
@bold = false
|
|
180
|
+
# if we are inside a link (i.e. to be output as <a> tag), _don't_ attempt to parse any links within that link
|
|
181
|
+
@parsers = if weAreInsideALink then @@linkTextParsers else @@fullTextParsers end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Process plain text by writing out HTML-escaped text
|
|
185
|
+
def processPlainText(match)
|
|
186
|
+
@writer.write(html_escape(match[0]))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Process a backslash-quoted character by writing it out as HTML-escaped text
|
|
190
|
+
def processBackslash(match)
|
|
191
|
+
@writer.write(html_escape(match[1]))
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Process an HTML entity by writing it out as is
|
|
195
|
+
def processEntity(match)
|
|
196
|
+
@writer.write(match[0])
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
# Process a double asterix by either starting or finishing an HTML bold section.
|
|
200
|
+
def processDoubleAsterix(match)
|
|
201
|
+
if @bold then
|
|
202
|
+
@writer.write("</b>")
|
|
203
|
+
@bold = false
|
|
204
|
+
else
|
|
205
|
+
@writer.write("<b>")
|
|
206
|
+
@bold = true
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Process a single asterix by either starting or finishing an HTML italic section.
|
|
211
|
+
def processSingleAsterix(match)
|
|
212
|
+
if @italic then
|
|
213
|
+
@writer.write("</i>")
|
|
214
|
+
@italic = false
|
|
215
|
+
else
|
|
216
|
+
@writer.write("<i>")
|
|
217
|
+
@italic = true
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Process a link definition which consists of a URL definition followed by a text definition
|
|
222
|
+
# Special cases of a URL definition are
|
|
223
|
+
# 1. Footnote, represented by a unique footnote identifier followed by '::'
|
|
224
|
+
# 2. Anchor link, represented by the anchor name followed by ':'
|
|
225
|
+
# The text definition is recursively parsed, except that link and anchor definitions
|
|
226
|
+
# cannot occur inside the text definition (or rather, they are just ignored).
|
|
227
|
+
def processLink (text, url)
|
|
228
|
+
anchorMatch = /^([^\/:]*):$/.match(url)
|
|
229
|
+
footnoteMatch = /^([^\/:]*)::$/.match(url)
|
|
230
|
+
linkTextHtml = @document.processText(text, :weAreInsideALink => true)
|
|
231
|
+
if footnoteMatch then
|
|
232
|
+
footnoteName = footnoteMatch[1]
|
|
233
|
+
# The footnote has a name (i.e. unique identifier) in the source code, but the footnotes
|
|
234
|
+
# are assigned sequential numbers in the output text.
|
|
235
|
+
footnoteNumber = @document.getNewFootnoteNumberFor(footnoteName)
|
|
236
|
+
@writer.write("<a href=\"##{footnoteName}\" class=\"footnote\">#{footnoteNumber}</a>")
|
|
237
|
+
elsif anchorMatch then
|
|
238
|
+
@writer.write("<a href=\"##{anchorMatch[1]}\">#{linkTextHtml}</a>")
|
|
239
|
+
else
|
|
240
|
+
@writer.write("<a href=\"#{url}\">#{linkTextHtml}</a>")
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
# Process an anchor definition which consists of either:
|
|
245
|
+
# 1. An normal anchor definition consisting of the anchor name followed by ':', or,
|
|
246
|
+
# 2. A footnote, consisting of the footnote identifier followed by '::' (the footnote identifier is also
|
|
247
|
+
# the anchor name) This is output as the actual footnote number (assigned previously when a link to the
|
|
248
|
+
# footnote was given), and the HTML anchor.
|
|
249
|
+
def processAnchor(url)
|
|
250
|
+
anchorMatch = /^([^\/:]*):$/.match(url)
|
|
251
|
+
if anchorMatch
|
|
252
|
+
@writer.write("<a name=\"#{anchorMatch[1]}\"></a>")
|
|
253
|
+
else
|
|
254
|
+
footnoteMatch = /^([^\/:]*)::$/.match(url)
|
|
255
|
+
if footnoteMatch
|
|
256
|
+
footnoteName = footnoteMatch[1]
|
|
257
|
+
footnoteNumberString = @document.footnoteNumberFor(footnoteName)
|
|
258
|
+
@writer.write("<span class=\"footnoteNumber\">#{footnoteNumberString}</span>" +
|
|
259
|
+
"<a name=\"#{footnoteName}\"></a>")
|
|
260
|
+
else
|
|
261
|
+
raise DocumentError, "Invalid URL for anchor: #{url.inspect}"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Process a link consisting of [] and optional () section. If the () section is not given,
|
|
267
|
+
# then it is an HTML anchor definition (<a name>), otherwise it represents an HTML link (<a href>).
|
|
268
|
+
def processLinkOrAnchor(match)
|
|
269
|
+
if match[3] then
|
|
270
|
+
processLink(match[1], match[3])
|
|
271
|
+
else
|
|
272
|
+
processAnchor(match[1])
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# If the '*' or '**' values are not balanced, complain.
|
|
277
|
+
def checkValidAtEnd
|
|
278
|
+
if @bold then
|
|
279
|
+
raise DocumentError, "unclosed bold span"
|
|
280
|
+
end
|
|
281
|
+
if @italic then
|
|
282
|
+
raise DocumentError, "unclosed italic span"
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
# Having parsed some of the text, how much is left to be parsed?
|
|
287
|
+
def textNotYetParsed
|
|
288
|
+
return @text[@pos..-1]
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
# Parse the source text by repeatedly parsing however much is matched by the first parsing
|
|
292
|
+
# rule in the list of parsing rules that matches anything.
|
|
293
|
+
# Each time, the parsers are applied in order of priority,
|
|
294
|
+
# until a first match is found. This match uses up whatever amount of source
|
|
295
|
+
# text it matched.
|
|
296
|
+
# This is repeated until the source code is all used up.
|
|
297
|
+
def parse
|
|
298
|
+
#puts "\nPARSING text #{@text.inspect} ..."
|
|
299
|
+
while @pos < @text.length do
|
|
300
|
+
#puts " parsing remaining text #{textNotYetParsed.inspect} ..."
|
|
301
|
+
match = nil
|
|
302
|
+
i = 0
|
|
303
|
+
textToParse = textNotYetParsed
|
|
304
|
+
# Try the specified parsers in order of priority, stopping at the first match
|
|
305
|
+
while i < @parsers.length and not match
|
|
306
|
+
parser = @parsers[i]
|
|
307
|
+
#puts " trying #{parser[1]} ..."
|
|
308
|
+
match = parser[0].match(textToParse)
|
|
309
|
+
i += 1
|
|
310
|
+
end
|
|
311
|
+
if match then
|
|
312
|
+
send(parser[1], match)
|
|
313
|
+
fullMatchOffsets = match.offset(0)
|
|
314
|
+
#puts " matched at #{fullMatchOffsets.inspect}, i.e. #{textToParse[fullMatchOffsets[0]...fullMatchOffsets[1]].inspect}"
|
|
315
|
+
@pos += fullMatchOffsets[1]
|
|
316
|
+
else
|
|
317
|
+
raise Exception, "No match on #{textNotYetParsed.inspect}"
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# An error object representing an error in the source code
|
|
325
|
+
class DocumentError < Exception
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# An object representing the propositional document which will be created by
|
|
329
|
+
# passing in source code, and then invoking the parsers to process the source code
|
|
330
|
+
# and output the HTML.
|
|
331
|
+
# The propositional document has three sections:
|
|
332
|
+
# 1. The introduction ("intro")
|
|
333
|
+
# 2. The list of propositions
|
|
334
|
+
# 3. An (optional) appendix
|
|
335
|
+
# The document also keeps track of named and numbered footnotes.
|
|
336
|
+
# The document has three additional required properties, which are "author", "title" and "date".
|
|
337
|
+
|
|
338
|
+
class PropositionalDocument
|
|
339
|
+
attr_reader :cursor, :fileName
|
|
340
|
+
|
|
341
|
+
include Helpers
|
|
342
|
+
|
|
343
|
+
def initialize(properties = {})
|
|
344
|
+
@properties = properties
|
|
345
|
+
@cursor = :intro #where the document is being written to currently
|
|
346
|
+
@intro = []
|
|
347
|
+
@propositions = []
|
|
348
|
+
@appendix = []
|
|
349
|
+
@footnoteCount = 0
|
|
350
|
+
@footnoteCountByName = {}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def getNewFootnoteNumberFor(footnoteName)
|
|
354
|
+
@footnoteCount += 1
|
|
355
|
+
footnoteCountString = @footnoteCount.to_s.to_s
|
|
356
|
+
@footnoteCountByName[footnoteName] = footnoteCountString
|
|
357
|
+
return footnoteCountString
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def footnoteNumberFor(footnoteName)
|
|
361
|
+
return @footnoteCountByName[footnoteName] || "?"
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def checkForProperty(name)
|
|
365
|
+
if not @properties.has_key?(name)
|
|
366
|
+
raise DocumentError, "No property #{name} given for document"
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
# Check that all the required properties were defined, and that at least one proposition occurred
|
|
371
|
+
def checkIsValid
|
|
372
|
+
checkForProperty("title")
|
|
373
|
+
checkForProperty("author")
|
|
374
|
+
checkForProperty("date")
|
|
375
|
+
if @cursor == :intro
|
|
376
|
+
raise DocumentError, "There are no propositions in the document"
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# Dump to stdout (for debugging/tracing)
|
|
381
|
+
def dump
|
|
382
|
+
puts "======================================================="
|
|
383
|
+
puts "Title: #{title.inspect}"
|
|
384
|
+
puts "Author: #{author.inspect}"
|
|
385
|
+
puts "Date: #{date.inspect}"
|
|
386
|
+
puts ""
|
|
387
|
+
if @intro.length > 0 then
|
|
388
|
+
puts "Introduction:"
|
|
389
|
+
for item in @intro do
|
|
390
|
+
puts " #{item}"
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
puts "Propositions:"
|
|
394
|
+
for item in @propositions do
|
|
395
|
+
item.dump(" ")
|
|
396
|
+
end
|
|
397
|
+
if @appendix.length > 0 then
|
|
398
|
+
puts "Appendix:"
|
|
399
|
+
for item in @appendix do
|
|
400
|
+
puts " #{item}"
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
puts "======================================================="
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Call this method in HTML template to write the title (and main heading)
|
|
407
|
+
def title
|
|
408
|
+
return @properties["title"]
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
# Call this method in HTML template to write the author's name
|
|
412
|
+
def author
|
|
413
|
+
return @properties["author"]
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# Call this method in HTML template to show the date
|
|
417
|
+
def date
|
|
418
|
+
return @properties["date"]
|
|
419
|
+
end
|
|
420
|
+
|
|
421
|
+
# Call this method in HTML template to render the introduction
|
|
422
|
+
def introHtml
|
|
423
|
+
return "<div class=\"intro\">\n#{@intro.map(&:toHtml).join("\n")}\n</div>"
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Call this method in HTML template to render the list of propositions
|
|
427
|
+
def propositionsHtml
|
|
428
|
+
return "<ul class=\"propositions\">\n#{@propositions.map(&:toHtml).join("\n")}\n</ul>"
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Call this method in HTML template to render the appendix
|
|
432
|
+
def appendixHtml
|
|
433
|
+
if @appendix.length == 0 then
|
|
434
|
+
return ""
|
|
435
|
+
else
|
|
436
|
+
return "<div class=\"appendix\">\n#{@appendix.map(&:toHtml).join("\n")}\n</div>"
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Call this method in HTML template to render the 'original link' (for an critique)
|
|
441
|
+
def originalLinkHtml
|
|
442
|
+
if @properties.has_key? "original-link" then
|
|
443
|
+
originalLinkText = @properties["original-link"]
|
|
444
|
+
html = "<div class=\"original-link\">#{processText(originalLinkText)}</div>"
|
|
445
|
+
puts " html = #{html.inspect}"
|
|
446
|
+
return html
|
|
447
|
+
else
|
|
448
|
+
return ""
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Generate the output HTML using an ERB template file, where the template references
|
|
453
|
+
# the methods title, author, date, propositionsHtml, appendixHtml, originalLinkHtml and fileName
|
|
454
|
+
# (applying them to 'self').
|
|
455
|
+
def generateHtml(templateFileName, fileName)
|
|
456
|
+
@fileName = fileName
|
|
457
|
+
puts " using template file #{templateFileName} ..."
|
|
458
|
+
templateText = File.read(templateFileName, encoding: 'UTF-8')
|
|
459
|
+
template = ERB.new(templateText)
|
|
460
|
+
@binding = binding
|
|
461
|
+
html = template.result(@binding)
|
|
462
|
+
return html
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Set a property value on the document
|
|
466
|
+
def setProperty(name, value)
|
|
467
|
+
@properties[name] = value
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
# Add a new proposition (only if we are currently in either the introduction or the propositions)
|
|
471
|
+
def addProposition(proposition)
|
|
472
|
+
case @cursor
|
|
473
|
+
when :intro
|
|
474
|
+
@cursor = :propositions
|
|
475
|
+
when :appendix
|
|
476
|
+
raise DocumentError, "Cannot add proposition, already in appendix"
|
|
477
|
+
end
|
|
478
|
+
proposition = PropositionWithExplanation.new(proposition)
|
|
479
|
+
@propositions.push(proposition)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Start the appendix (but only if we are currently in the propositions)
|
|
483
|
+
def startAppendix
|
|
484
|
+
case @cursor
|
|
485
|
+
when :intro
|
|
486
|
+
raise DocumentError, "Cannot start appendix before any propositions occur"
|
|
487
|
+
when :propositions
|
|
488
|
+
@cursor = :appendix
|
|
489
|
+
when :appendix
|
|
490
|
+
raise DocumentError, "Cannot start appendix, already in appendix"
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
|
|
494
|
+
# Add a textual item, depending on where we are, to the introduction, or the current proposition,
|
|
495
|
+
# or to the appendix
|
|
496
|
+
def addText(text)
|
|
497
|
+
case @cursor
|
|
498
|
+
when :intro
|
|
499
|
+
@intro.push(text)
|
|
500
|
+
when :propositions
|
|
501
|
+
@propositions[-1].addExplanationItem(text)
|
|
502
|
+
when :appendix
|
|
503
|
+
@appendix.push(text)
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Add a heading (but only to the appendix - note that propositional headings are dealt with separately,
|
|
508
|
+
# because the heading of a proposition defines a new proposition)
|
|
509
|
+
def addHeading(heading)
|
|
510
|
+
case @cursor
|
|
511
|
+
when :intro
|
|
512
|
+
raise DocumentError, "Headings are not allowed in the introduction"
|
|
513
|
+
when :propositions
|
|
514
|
+
raise DocumentError, "Headings are not allowed in propositions"
|
|
515
|
+
when :appendix
|
|
516
|
+
@appendix.push(heading)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
# Special text replacements done after all other processing,
|
|
521
|
+
# currently just "--" => –
|
|
522
|
+
def doExtraTextReplacements(text)
|
|
523
|
+
#puts "doExtraTextReplacements on #{text.inspect} ..."
|
|
524
|
+
text.gsub!(/--/m, "–")
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
# Process text. "text" can occur in five different contexts:
|
|
528
|
+
# 1. Inside a link (where :weAreInsideALink gets set to true)
|
|
529
|
+
# 2. A proposition
|
|
530
|
+
# 3. A list item
|
|
531
|
+
# 4. A paragraph
|
|
532
|
+
# 5. A secondary heading (i.e. other than a proposition - currently these can only appear in the appendix)
|
|
533
|
+
def processText(text, options = {})
|
|
534
|
+
weAreInsideALink = options[:weAreInsideALink]
|
|
535
|
+
stringBuffer = StringBuffer.new()
|
|
536
|
+
TextBeingProcessed.new(self, text, stringBuffer, weAreInsideALink).parse()
|
|
537
|
+
processedText = stringBuffer.to_string()
|
|
538
|
+
doExtraTextReplacements(processedText)
|
|
539
|
+
return processedText
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
# Base class for top-level document components
|
|
545
|
+
class DocumentComponent
|
|
546
|
+
end
|
|
547
|
+
|
|
548
|
+
# A document property value definition, such as date = '23 May, 2014'
|
|
549
|
+
class DocumentProperty < DocumentComponent
|
|
550
|
+
def initialize(name, value)
|
|
551
|
+
@name = name
|
|
552
|
+
@value = value
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def to_s
|
|
556
|
+
return "DocumentProperty #{@name} = #{@value.inspect}"
|
|
557
|
+
end
|
|
558
|
+
|
|
559
|
+
# 'write' to the document by setting the specified property value
|
|
560
|
+
def writeToDocument(document)
|
|
561
|
+
document.setProperty(@name, @value)
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
# Instruction to start the appendix
|
|
566
|
+
class StartAppendix < DocumentComponent
|
|
567
|
+
def to_s
|
|
568
|
+
return "StartAppendix"
|
|
569
|
+
end
|
|
570
|
+
|
|
571
|
+
# 'write' to the document by updating document state to being in the appendix
|
|
572
|
+
def writeToDocument(document)
|
|
573
|
+
document.startAppendix
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# A proposition (just the heading, without the explanation)
|
|
578
|
+
class Proposition < DocumentComponent
|
|
579
|
+
def initialize(text)
|
|
580
|
+
@text = text
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
def to_s
|
|
584
|
+
return "Proposition: #{@text.inspect}"
|
|
585
|
+
end
|
|
586
|
+
|
|
587
|
+
# write to document, by adding a proposition to the document
|
|
588
|
+
# (this will set state to :proposition if we were in the :intro, close any previous proposition,
|
|
589
|
+
# and start a new one)
|
|
590
|
+
def writeToDocument(document)
|
|
591
|
+
document.addProposition(self)
|
|
592
|
+
@document = document
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def toHtml
|
|
596
|
+
return "<div class=\"proposition\">#{@document.processText(@text)}</div>"
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Base text is either a list of list items, or, a paragraph.
|
|
601
|
+
class BaseText < DocumentComponent
|
|
602
|
+
attr_reader :document
|
|
603
|
+
|
|
604
|
+
# write to document, by adding to the document as a text item. Depending on the
|
|
605
|
+
# current document state, this will be added to the introduction, or to the explanation of the
|
|
606
|
+
# current proposition, or to the appendix
|
|
607
|
+
def writeToDocument(document)
|
|
608
|
+
document.addText(self)
|
|
609
|
+
@document = document
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def critiqueClassHtml
|
|
613
|
+
if @isCritique then
|
|
614
|
+
return " class=\"critique\""
|
|
615
|
+
else
|
|
616
|
+
return ""
|
|
617
|
+
end
|
|
618
|
+
end
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
# A list item (note: this is not a top-level component, rather it is part of an ItemList component)
|
|
622
|
+
class ListItem
|
|
623
|
+
attr_accessor :list
|
|
624
|
+
|
|
625
|
+
def initialize(text)
|
|
626
|
+
@text = text
|
|
627
|
+
@list = nil
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def to_s
|
|
631
|
+
return "ListItem: #{@text.inspect}"
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def document
|
|
635
|
+
return list.document
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def toHtml
|
|
639
|
+
return "<li>#{document.processText(@text)}</li>"
|
|
640
|
+
end
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# A list of list items
|
|
644
|
+
class ItemList < BaseText
|
|
645
|
+
def initialize(options = {})
|
|
646
|
+
@isCritique = options[:isCritique] || false
|
|
647
|
+
@items = []
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
def addItem(listItem)
|
|
651
|
+
@items.push(listItem)
|
|
652
|
+
listItem.list = self
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def to_s
|
|
656
|
+
return "ItemList: #{@items.inspect}"
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def toHtml
|
|
660
|
+
return "<ul#{critiqueClassHtml}>\n#{@items.map(&:toHtml).join("\n")}\n</ul>"
|
|
661
|
+
end
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
# A paragraph.
|
|
665
|
+
class Paragraph < BaseText
|
|
666
|
+
@@tagsMap = {"bq" => {:tag => "blockquote"}}
|
|
667
|
+
|
|
668
|
+
def initialize(text, options = {})
|
|
669
|
+
@text = text
|
|
670
|
+
@isCritique = options[:isCritique] || false
|
|
671
|
+
@tag = options[:tag]
|
|
672
|
+
initializeStartEndTags
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def initializeStartEndTags
|
|
676
|
+
@tagName = "p"
|
|
677
|
+
className = nil
|
|
678
|
+
if @tag then
|
|
679
|
+
tagDescriptor = @@tagsMap[@tag]
|
|
680
|
+
if tagDescriptor == nil then
|
|
681
|
+
raise DocumentError, "Unknown tag: #{@tag.inspect}"
|
|
682
|
+
end
|
|
683
|
+
@tagName = tagDescriptor[:tag]
|
|
684
|
+
@className = tagDescriptor[:class]
|
|
685
|
+
end
|
|
686
|
+
classNames = []
|
|
687
|
+
if @isCritique then
|
|
688
|
+
classNames.push("critique")
|
|
689
|
+
end
|
|
690
|
+
if @classname then
|
|
691
|
+
classNames.push(@className)
|
|
692
|
+
end
|
|
693
|
+
@startTag = "<#{@tagName}#{classesHtml(classNames)}>"
|
|
694
|
+
@endTag = "</#{@tagName}>"
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def classesHtml(classNames)
|
|
698
|
+
if classNames.length == 0 then
|
|
699
|
+
return ""
|
|
700
|
+
else
|
|
701
|
+
return " class=\"#{classNames.join(" ")}\""
|
|
702
|
+
end
|
|
703
|
+
end
|
|
704
|
+
|
|
705
|
+
def to_s
|
|
706
|
+
return "Paragraph: #{@text.inspect}"
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
def toHtml
|
|
710
|
+
return "#{@startTag}#{@document.processText(@text)}#{@endTag}"
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
# A secondary heading (which can appear in the appendix)
|
|
715
|
+
class Heading < DocumentComponent
|
|
716
|
+
def initialize(text)
|
|
717
|
+
@text = text
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def to_s
|
|
721
|
+
return "Heading: #{@text}"
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
# write to document by adding a heading (which is only valid if we are in the appendix)
|
|
725
|
+
def writeToDocument(document)
|
|
726
|
+
document.addHeading(self)
|
|
727
|
+
@document = document
|
|
728
|
+
end
|
|
729
|
+
|
|
730
|
+
def toHtml
|
|
731
|
+
return "<h2>#{@document.processText(@text)}</h2>"
|
|
732
|
+
end
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# A "chunk" of text from the source, corresponding to a group of lines, either delimited
|
|
736
|
+
# by a blank line, or, identifiable as a special one-line chunk from the contents of the line
|
|
737
|
+
class LinesChunk
|
|
738
|
+
attr_reader :lines
|
|
739
|
+
|
|
740
|
+
def initialize(line, options = {})
|
|
741
|
+
@lines = [line]
|
|
742
|
+
@isCritique = options[:isCritique] || false
|
|
743
|
+
@tag = options[:tag]
|
|
744
|
+
end
|
|
745
|
+
|
|
746
|
+
def addLine(line)
|
|
747
|
+
@lines.push(line)
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
# a post-processing step in which a chunk might redefine itself as a different type of chunk
|
|
751
|
+
# based on information available only _after_ all the lines have been added to it
|
|
752
|
+
# (in particular the case where a "heading" is defined by the occurrence of "--------" in the second and last line).
|
|
753
|
+
def postProcess
|
|
754
|
+
return self
|
|
755
|
+
end
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
# Special chunk contains a name and an optional value
|
|
759
|
+
# examples - "##appendix", "##date 23 May, 2014" "##author John Smith"
|
|
760
|
+
class SpecialChunk < LinesChunk
|
|
761
|
+
attr_reader :name
|
|
762
|
+
def initialize(name, line)
|
|
763
|
+
super(line)
|
|
764
|
+
@name = name
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def to_s
|
|
768
|
+
return "SpecialChunk: (#{name}) #{lines.inspect}"
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
# "special chunk" is terminated by a blank line, or by the start of the next special chunk
|
|
772
|
+
def isTerminatedBy?(line)
|
|
773
|
+
return (/^\#\#/.match(line) or /^\s*$/.match(line))
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
def getDocumentComponent
|
|
777
|
+
if name == "appendix"
|
|
778
|
+
return StartAppendix.new() # special "start appendix" command, or,
|
|
779
|
+
else
|
|
780
|
+
return DocumentProperty.new(name, lines.join("\n")) # a named property value
|
|
781
|
+
end
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
# A chunk which is a group of lines terminated by a following blank line
|
|
787
|
+
class BlankTerminatedChunk < LinesChunk
|
|
788
|
+
def isTerminatedBy?(line)
|
|
789
|
+
return /^\s*$/.match(line)
|
|
790
|
+
end
|
|
791
|
+
|
|
792
|
+
def critiqueValue
|
|
793
|
+
if @isCritique then
|
|
794
|
+
return "(is critique) "
|
|
795
|
+
else
|
|
796
|
+
return ""
|
|
797
|
+
end
|
|
798
|
+
end
|
|
799
|
+
end
|
|
800
|
+
|
|
801
|
+
# A heading chunk is a group of two lines, the second of which is repeated '-' characters (i.e. the underlining of the heading)
|
|
802
|
+
class HeadingChunk
|
|
803
|
+
attr_reader :text
|
|
804
|
+
def initialize(text)
|
|
805
|
+
@text = text
|
|
806
|
+
end
|
|
807
|
+
|
|
808
|
+
def to_s
|
|
809
|
+
return "HeadingChunk: #{text.inspect}"
|
|
810
|
+
end
|
|
811
|
+
|
|
812
|
+
def getDocumentComponent
|
|
813
|
+
return Heading.new(text)
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# A paragraph chunk is a group of lines representing a paragraph (because it is not recognisable
|
|
818
|
+
# as anything else)
|
|
819
|
+
class ParagraphChunk < BlankTerminatedChunk
|
|
820
|
+
|
|
821
|
+
def to_s
|
|
822
|
+
return "ParagraphChunk: #{critiqueValue}#{lines.inspect}"
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Check if this paragraph is actually a heading, as determined by the 2nd line being an underline sequence
|
|
826
|
+
def postProcess
|
|
827
|
+
if lines.length == 2 and lines[1].match(/^[-]+$/) then
|
|
828
|
+
return HeadingChunk.new(lines[0])
|
|
829
|
+
else
|
|
830
|
+
return self
|
|
831
|
+
end
|
|
832
|
+
end
|
|
833
|
+
|
|
834
|
+
def getDocumentComponent
|
|
835
|
+
return Paragraph.new(lines.join("\n"), :isCritique => @isCritique, :tag => @tag)
|
|
836
|
+
end
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# A proposition chunk represents a proposition (recognised because the first line starts with "# ")
|
|
840
|
+
class PropositionChunk < BlankTerminatedChunk
|
|
841
|
+
def to_s
|
|
842
|
+
return "PropositionChunk: #{lines.inspect}"
|
|
843
|
+
end
|
|
844
|
+
|
|
845
|
+
def getDocumentComponent
|
|
846
|
+
return Proposition.new(lines.join("\n"))
|
|
847
|
+
end
|
|
848
|
+
end
|
|
849
|
+
|
|
850
|
+
# A list chunk represents a list of list items (recognised because the first line starts with "* ")
|
|
851
|
+
class ListChunk < BlankTerminatedChunk
|
|
852
|
+
def to_s
|
|
853
|
+
return "ListChunk: #{critiqueValue}#{lines.inspect}"
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
def addItemToList(itemList, currentItemLines)
|
|
857
|
+
itemList.addItem(ListItem.new(currentItemLines.join("\n")))
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def getDocumentComponent
|
|
861
|
+
itemList = ItemList.new(:isCritique => @isCritique)
|
|
862
|
+
currentItemLines = nil
|
|
863
|
+
# loop over lines
|
|
864
|
+
for line in lines do
|
|
865
|
+
itemStartMatch = line.match(/^\*\s+(.*)$/) # if a line starts with "* ",
|
|
866
|
+
if itemStartMatch # we have found the start of a new item
|
|
867
|
+
if currentItemLines != nil
|
|
868
|
+
addItemToList(itemList, currentItemLines) # save the previous list item if any
|
|
869
|
+
end
|
|
870
|
+
currentItemLines = [itemStartMatch[1]] # start the new list of lines for this item
|
|
871
|
+
else
|
|
872
|
+
currentItemLines.push(line.strip) # add this line to the existing list of lines for the current list item
|
|
873
|
+
end
|
|
874
|
+
end
|
|
875
|
+
if currentItemLines != nil
|
|
876
|
+
addItemToList(itemList, currentItemLines) # save any final list item not yet saved
|
|
877
|
+
end
|
|
878
|
+
return itemList
|
|
879
|
+
end
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Propolize source code is parsed as a series of "document chunks".
|
|
883
|
+
# (Each chunk is then converted to a "document component" by calling getDocumentComponent
|
|
884
|
+
# and each component is then "written" to the output document by calling writeToDocument.)
|
|
885
|
+
class DocumentChunks
|
|
886
|
+
def initialize(srcText)
|
|
887
|
+
@srcText = srcText
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
# Knowing that we are starting a new "chunk", determine type of chunk based on the first line
|
|
891
|
+
def createInitialChunkFromLine(line)
|
|
892
|
+
specialLineMatch = line.match(/^\#\#([a-z\-]*)\s*(.*)$/) # does it start with "##<identifier>" with optional addition value?, and identifier is alphabetic or "-"?
|
|
893
|
+
if specialLineMatch then
|
|
894
|
+
return SpecialChunk.new(specialLineMatch[1], specialLineMatch[2])
|
|
895
|
+
else
|
|
896
|
+
specialTagMatch = line.match(/^\#:([a-z\-0-9]+)\s*(.*)$/) # does it start with "#:<alphanumeric-identifier>"?
|
|
897
|
+
if specialTagMatch then
|
|
898
|
+
return ParagraphChunk.new(specialTagMatch[2], :tag => specialTagMatch[1]) # paragraph with special tag
|
|
899
|
+
else
|
|
900
|
+
propositionMatch = line.match(/^\#\s*(.*)$/) # does it start with "# "?
|
|
901
|
+
if propositionMatch then
|
|
902
|
+
return PropositionChunk.new(propositionMatch[1]) # proposition
|
|
903
|
+
else
|
|
904
|
+
critiqueMatch = line.match(/^\?\?\s(.*)$/) # does it start with "?? " ?
|
|
905
|
+
isCritique = critiqueMatch != nil # if so, this is a "critique" item
|
|
906
|
+
if isCritique then
|
|
907
|
+
line = critiqueMatch[1] # strip off the "?? " prefix to process the rest of the line
|
|
908
|
+
end
|
|
909
|
+
listMatch = line.match(/^\*\s+/) # does it start with "* " ?
|
|
910
|
+
if listMatch then
|
|
911
|
+
return ListChunk.new(line, :isCritique => isCritique) # list of items
|
|
912
|
+
else
|
|
913
|
+
blankLineMatch = line.match(/^\s*$/) # is it a blank line
|
|
914
|
+
if blankLineMatch then
|
|
915
|
+
return nil # blank line, so actually we don't start a new chunk yet
|
|
916
|
+
else
|
|
917
|
+
return ParagraphChunk.new(line, :isCritique => isCritique) # anything else, must be a paragraph
|
|
918
|
+
end
|
|
919
|
+
end
|
|
920
|
+
end
|
|
921
|
+
end
|
|
922
|
+
end
|
|
923
|
+
end
|
|
924
|
+
|
|
925
|
+
def each
|
|
926
|
+
currentChunk = nil
|
|
927
|
+
for line in @srcText.split("\n") do
|
|
928
|
+
if currentChunk == nil then
|
|
929
|
+
currentChunk = createInitialChunkFromLine(line)
|
|
930
|
+
else
|
|
931
|
+
if currentChunk.isTerminatedBy?(line)
|
|
932
|
+
yield currentChunk.postProcess
|
|
933
|
+
currentChunk = createInitialChunkFromLine(line)
|
|
934
|
+
else
|
|
935
|
+
currentChunk.addLine(line)
|
|
936
|
+
end
|
|
937
|
+
end
|
|
938
|
+
end
|
|
939
|
+
if currentChunk
|
|
940
|
+
yield currentChunk.postProcess
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
end
|
|
944
|
+
|
|
945
|
+
# Top-level class to provide the "propolize" method to be called by client code
|
|
946
|
+
class Propolizer
|
|
947
|
+
|
|
948
|
+
# Main method to generated the HTML document from the provided source text
|
|
949
|
+
def propolize(templateFileName, srcText, fileName, properties = {})
|
|
950
|
+
document = PropositionalDocument.new(properties)
|
|
951
|
+
for chunk in DocumentChunks.new(srcText) do
|
|
952
|
+
#puts "#{chunk}"
|
|
953
|
+
component = chunk.getDocumentComponent
|
|
954
|
+
#puts " => #{component}"
|
|
955
|
+
component.writeToDocument(document)
|
|
956
|
+
end
|
|
957
|
+
document.checkIsValid()
|
|
958
|
+
#document.dump
|
|
959
|
+
|
|
960
|
+
return document.generateHtml(templateFileName, File.basename(fileName))
|
|
961
|
+
end
|
|
962
|
+
end
|
|
963
|
+
end
|