regenerate 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,4 +1,5 @@
1
- # A framework for static website generation which regenerates files in place.
1
+ # A framework for static website generation which regenerates files in place
2
+ # (or, if you like, generates them into a different place)
2
3
 
3
4
  require 'regenerate/web-page.rb'
4
5
  require 'regenerate/site-regenerator.rb'
@@ -2,6 +2,8 @@ module Regenerate
2
2
 
3
3
  module Utils
4
4
 
5
+ # If an old version of an output file exists, rename it to same file with "~" at the end.
6
+ # If that file (an earlier backup file) exists, delete it first.
5
7
  def makeBackupFile(outFile)
6
8
  backupFileName = outFile+"~"
7
9
  if File.exists? backupFileName
@@ -15,6 +17,8 @@ module Regenerate
15
17
  backupFileName
16
18
  end
17
19
 
20
+ # Cause a directory to be created if it does not already exist.
21
+ # Raise an error if it does not exist and it cannot be created.
18
22
  def ensureDirectoryExists(directoryName)
19
23
  if File.exist? directoryName
20
24
  if not File.directory? directoryName
@@ -4,6 +4,7 @@ require 'regenerate/regenerate-utils.rb'
4
4
 
5
5
  module Regenerate
6
6
 
7
+ # An object to iterate over a directory path and all its parent directories
7
8
  class PathAndParents
8
9
  def initialize(path)
9
10
  @path = path
@@ -21,14 +22,24 @@ module Regenerate
21
22
  end
22
23
  end
23
24
 
25
+ # The main object representing the static website source and output directories
26
+ # where generation or regeneration will occur.
27
+ # There are three types of generation/regeneration, depending on how it is invoked:
28
+ # 1. Re-generate source file or files in-place
29
+ # 2. Generate output file or files from source file or files
30
+ # 3. Re-generate source file or files from output file or files (this is for when output files have been directly edited,
31
+ # and when it is possible to re-generate the source)
24
32
  class SiteRegenerator
25
33
 
26
34
  include Regenerate::Utils
27
35
 
28
- attr_accessor :checkNoChanges
29
-
36
+ # an option to check for changes and throw an error before an existing output file is changed
37
+ # (use this option to test that certain changes in your code _don't_ change the result for your website)
38
+ attr_accessor :checkNoChanges
39
+
40
+ # Initialise giving base directory of project, and sub-directories for source and output
41
+ # e.g. "/home/me/myproject", "src" and "output"
30
42
  def initialize(baseDir, sourceSubDir, outputSubDir)
31
-
32
43
  @baseDir = File.expand_path(baseDir)
33
44
  @sourceSubDir = sourceSubDir
34
45
  @outputSubDir = outputSubDir
@@ -40,6 +51,7 @@ module Regenerate
40
51
  puts "SiteRegenerator, @baseDir = #{@baseDir.inspect}"
41
52
  end
42
53
 
54
+ # files & directories starting with "_" are not output files (they are other helper files)
43
55
  def checkNotSourceOnly(pathComponents)
44
56
  for component in pathComponents do
45
57
  if component.start_with?("_")
@@ -48,13 +60,17 @@ module Regenerate
48
60
  end
49
61
  end
50
62
 
63
+ # Extensions for types of files to be generated/regenerated
51
64
  REGENERATE_EXTENSIONS = [".htm", ".html", ".xml"]
52
65
 
66
+ # Copy a source file directly to an output file
53
67
  def copySrcToOutputFile(srcFile, outFile)
54
68
  makeBackupFile(outFile)
55
69
  FileUtils.cp(srcFile, outFile, :verbose => true)
56
70
  end
57
71
 
72
+ # Generate an output file from a source file
73
+ # (pathComponents represent the path from the root source directory to the actual file)
58
74
  def regenerateFileFromSource(srcFile, pathComponents)
59
75
  puts "regenerateFileFromSource, srcFile = #{srcFile}, pathComponents = #{pathComponents.inspect}"
60
76
  subPath = pathComponents.join("/")
@@ -70,6 +86,7 @@ module Regenerate
70
86
  end
71
87
  end
72
88
 
89
+ # Generate a source file from an output file (if that can be done)
73
90
  def regenerateSourceFromOutput(outFile, pathComponents)
74
91
  puts "regenerateSourceFromOutput, outFile = #{outFile}, pathComponents = #{pathComponents.inspect}"
75
92
  subPath = pathComponents.join("/")
@@ -85,6 +102,7 @@ module Regenerate
85
102
  end
86
103
  end
87
104
 
105
+ # Regenerate (or generate) a file, either from source file or from output file
88
106
  def regenerateFile(srcFile, pathComponents, sourceType)
89
107
  puts "regenerateFile, srcFile = #{srcFile}, sourceType = #{sourceType.inspect}"
90
108
  outFile = File.join(@sourceTypeDirs[@oppositeSourceType[sourceType]], File.join(pathComponents))
@@ -103,6 +121,8 @@ module Regenerate
103
121
  end
104
122
  end
105
123
 
124
+ # Regenerate (or generated) specified sub-directory or file in sub-directory
125
+ # of source or output root directory (according to sourceType)
106
126
  def regenerateSubPath(pathComponents, sourceType)
107
127
  puts "regenerateSubPath, pathComponents = #{pathComponents.inspect}, sourceType = #{sourceType.inspect}"
108
128
  srcPath = File.join(@sourceTypeDirs[sourceType], File.join(pathComponents))
@@ -120,6 +140,8 @@ module Regenerate
120
140
  end
121
141
  end
122
142
 
143
+ # Regenerate (or generate) from specified source file (according to whether the path is within
144
+ # the source or output root directory).
123
145
  def regeneratePath(path)
124
146
  path = File.expand_path(path)
125
147
  puts "SiteRegenerator.regeneratePath, path = #{path}"
@@ -141,6 +163,7 @@ module Regenerate
141
163
 
142
164
  end
143
165
 
166
+ # Searching upwards from the current directory, find a file ".regenerate.rb" in the root directory of the project
144
167
  def self.findRegenerateScript(path, fileName)
145
168
  for dir in PathAndParents.new(path) do
146
169
  scriptFileName = File.join(dir, fileName)
@@ -152,6 +175,11 @@ module Regenerate
152
175
  raise "File #{fileName} not found in #{path} or any or its parent directories"
153
176
  end
154
177
 
178
+ # Run the ".regenerate.rb" script that is in the root directory of the project
179
+ # (Note: .regenerate.rb is responsible for requiring Ruby scripts that define Ruby classes specific to the project,
180
+ # for creating a SiteRegenerator instance, and for invoking the regeneratePath method
181
+ # on a file or directory name that has been set as the value of the "path" variable in the binding within which
182
+ # .regenerate.rb is being evaluated.)
155
183
  def self.regeneratePath(path)
156
184
  regenerateScriptFileName = findRegenerateScript(path, ".regenerate.rb")
157
185
  regenerateScript = File.read(regenerateScriptFileName)
@@ -5,74 +5,93 @@ require 'regenerate/regenerate-utils.rb'
5
5
 
6
6
  module Regenerate
7
7
 
8
- # A component, which includes a sequence of lines which make up the text of that component
8
+ # The textual format for "regeneratable" files is HTML (or XML) with special comment lines that mark the beginnings
9
+ # and ends of particular "page components". Such components may include actual HTML (or XML) in which case there
10
+ # are special start & end comment lines, or, they may consist entirely of one multi-line comment.
11
+
12
+ # Base class for page components, defined by a sequence of lines in an HTML file which make up the text of that component
9
13
  class PageComponent
10
14
  attr_reader :text
11
15
  attr_accessor :parentPage
12
16
 
13
17
  def initialize
14
- @lines = []
18
+ @lines = [] # No lines of text yet
15
19
  @text = nil # if text is nil, component is not yet finished
16
- @parentPage = nil
20
+ @parentPage = nil # A link to the parent WebPage object, will get set from a method on that object
17
21
  end
18
22
 
19
23
  def processStartComment(parsedCommentLine)
20
- @startName = parsedCommentLine.name
21
- initializeFromStartComment(parsedCommentLine)
24
+ @startName = parsedCommentLine.name # remember the name in the start command,
25
+ # because it has to match the name in the end command
26
+ initializeFromStartComment(parsedCommentLine) # do whatever has to be done to initialise this page component
22
27
  if parsedCommentLine.sectionEnd # section end in start comment line, so already finished
23
- finishText
28
+ finishText # i.e. there will be no more text lines added to this component
24
29
  end
25
30
  end
26
31
 
27
32
  def processEndComment(parsedCommentLine)
28
- finishText
29
- if parsedCommentLine.name != @startName
33
+ finishText # there will be no more text lines added to this component
34
+ if parsedCommentLine.name != @startName # check match of name in end comment with name in start comment
30
35
  raise ParseException.new("Name #{parsedCommentLine.name.inspect} in end comment doesn't match name #{@startName.inspect} in start comment.")
31
36
  end
32
37
  end
33
-
38
+
39
+ # Do whatever needs to be done to initialise this page component from the start command
34
40
  def initializeFromStartComment(parsedCommentLine)
35
- # default do nothing
41
+ # default do nothing - over-ride this method in derived classes
36
42
  end
37
43
 
44
+ # Has this component finished
38
45
  def finished
39
- @text != nil
46
+ @text != nil # finishText sets the value of @text, so use that as a test for "is it finished?"
40
47
  end
41
48
 
49
+ # Add a text line, by adding it to @lines
42
50
  def addLine(line)
43
51
  @lines << line
44
52
  end
45
53
 
54
+ # Do whatever needs to be done to the parent WebPage object for this page component
46
55
  def addToParentPage
47
- # default do nothing
56
+ # default do nothing - over-ride this in derived classes
48
57
  end
49
58
 
59
+ # After all text lines have been added, join them together and put into @text
50
60
  def finishText
51
61
  @text = @lines.join("\n")
52
- addToParentPage
62
+ addToParentPage # do whatever needs to be done to the parent WebPage object for this page component
53
63
  end
54
64
  end
55
65
 
56
- # A component of static text which is not assigned to any variable, and which does not change
66
+ # A page component of static text which is not assigned to any variable, and which does not change when re-generated.
67
+ # Any sequence of line _not_ marked by special start and end lines will constitute static HTML.
57
68
  class StaticHtml < PageComponent
69
+
70
+ # output the text as is (but with an extra eoln)
58
71
  def output(showSource = true)
59
72
  text + "\n"
60
73
  end
61
74
 
75
+ # varName is nil because no instance variable is associated with a static HTML page component
62
76
  def varName
63
77
  nil
64
78
  end
65
79
  end
66
80
 
67
- class RubyCode<PageComponent
81
+ # A page component consisting of Ruby code which is to be evaluated in the context of the object that defines the page
82
+ # Defined by start line "<!-- [ruby" and end line "ruby] -->".
83
+ class RubyCode < PageComponent
68
84
 
85
+ # The line number, which matters for code evaluation so that Ruby can populate stack traces correctly
69
86
  attr_reader :lineNumber
70
87
 
88
+ # Initialise this page component, additionally initialising the line number
71
89
  def initialize(lineNumber)
72
90
  super()
73
91
  @lineNumber = lineNumber
74
92
  end
75
93
 
94
+ # Output this page component in a form that would be reparsed back into the same page component
76
95
  def output(showSource = true)
77
96
  if showSource
78
97
  "<!-- [ruby\n#{text}\nruby] -->\n"
@@ -81,18 +100,27 @@ module Regenerate
81
100
  end
82
101
  end
83
102
 
103
+ # Add to parent page, which requires adding to the page's list of Ruby page components (which will all
104
+ # get executed at some later stage)
84
105
  def addToParentPage
85
106
  @parentPage.addRubyComponent(self)
86
107
  end
87
108
  end
88
-
89
- class SetPageObjectClass<PageComponent
90
- attr_reader :className
109
+
110
+ # A page component consisting of a single line which specifies the Ruby class which represents this page
111
+ # In the format "<!-- [class <classname>] -->" where <classname> is the name of a Ruby class.
112
+ # The Ruby class should generally have Regenerate::PageObject as a base class.
113
+ class SetPageObjectClass < PageComponent
114
+
115
+ attr_reader :className # The name of the Ruby class of the page object
116
+
117
+ # Initialise this page component, additionally initialising the class name
91
118
  def initialize(className)
92
119
  super()
93
120
  @className = className
94
121
  end
95
122
 
123
+ # Output this page component in a form that would be reparsed back into the same page component
96
124
  def output(showSource = true)
97
125
  if showSource
98
126
  "<!-- [class #{@className}] -->\n"
@@ -102,31 +130,39 @@ module Regenerate
102
130
  end
103
131
 
104
132
  def addToParentPage
133
+ # Add to parent page, which creates a new page object of the specified class (replacing the default PageObject object)
105
134
  @parentPage.setPageObject(@className)
106
135
  end
107
136
  end
108
137
 
109
- # Base class for the text variable types
138
+ # Base class for a page component defining a block of text (which may or may not be inside a comment)
139
+ # which is assigned to an instance variable of the object representing the page. (The text value for
140
+ # this instance variable may be both read and written by the Ruby code that runs in the context of the object.)
110
141
  class TextVariable<PageComponent
111
- attr_reader :varName
142
+ attr_reader :varName # the name of the instance variable of the page object that will hold this value
112
143
 
144
+ # initialise, which sets the instance variable name
113
145
  def initializeFromStartComment(parsedCommentLine)
114
146
  @varName = parsedCommentLine.instanceVarName
115
147
  end
116
148
 
149
+ # add to parent WebPage by adding the specified instance variable to the page object
117
150
  def addToParentPage
118
151
  #puts "TextVariable.addToParentPage #{@varName} = #{@text.inspect}"
119
152
  @parentPage.setPageObjectInstanceVar(@varName, @text)
120
153
  end
121
154
 
155
+ # Get the textual value of the page object instance variable
122
156
  def textVariableValue
123
157
  @parentPage.getPageObjectInstanceVar(@varName)
124
158
  end
125
159
  end
126
160
 
127
- # HtmlVariable Can be both source and result
161
+ # A page component for a block of HTML which is assigned to an instance variable of the page object
128
162
  class HtmlVariable < TextVariable
129
163
 
164
+ # Process the end command by finishing the page component definition, also check that the closing
165
+ # command has the correct form (must be a self-contained HTML comment line)
130
166
  def processEndComment(parsedCommentLine)
131
167
  super(parsedCommentLine)
132
168
  if !parsedCommentLine.hasCommentStart
@@ -134,6 +170,8 @@ module Regenerate
134
170
  end
135
171
  end
136
172
 
173
+ # Output in a form that would be reparsed as the same page component, but with whatever the current
174
+ # textual value of the associate page object instance variable is.
137
175
  def output(showSource = true)
138
176
  if showSource
139
177
  textValue = textVariableValue
@@ -148,8 +186,11 @@ module Regenerate
148
186
  end
149
187
  end
150
188
 
151
- # CommentVariable Is an input only
189
+ # A page component for text inside an HTML comment which is assigned to an instance variable of the page object
152
190
  class CommentVariable < TextVariable
191
+
192
+ # Process the end command by finishing the page component definition, also check that the closing
193
+ # command has the correct form (must be a line that ends an existing HTML comment)
153
194
  def processEndComment(parsedCommentLine)
154
195
  super(parsedCommentLine)
155
196
  if parsedCommentLine.hasCommentStart
@@ -157,6 +198,8 @@ module Regenerate
157
198
  end
158
199
  end
159
200
 
201
+ # Output in a form that would be reparsed as the same page component, but with whatever the current
202
+ # textual value of the associate page object instance variable is.
160
203
  def output(showSource = true)
161
204
  if showSource
162
205
  "<!-- [#{@varName}\n#{textVariableValue}\n#{@varName}] -->\n"
@@ -166,15 +209,51 @@ module Regenerate
166
209
  end
167
210
  end
168
211
 
169
- COMMENT_LINE_REGEX = /^\s*(<!--\s*|)(\[|)((@|)[_a-zA-Z][_a-zA-Z0-9]*)(|\s+([_a-zA-Z0-9]*))(\]|)(\s*-->|)?\s*$/
212
+ # Regex that matches Regenerate comment line, with following components:
213
+ # * Optional whitespace
214
+ # * Possible ("<!--" + optional whitespace)
215
+ # * Possible "["
216
+ # * Possible Ruby identifier (alphanumeric or underscore with initial non-numeric) with optional "@" prefix
217
+ # * Possible (whitespace + alphanumeric/underscore value)
218
+ # * Possible "]"
219
+ # * Possible (optional whitespace + "-->")
220
+ # * Optional whitespace
221
+ COMMENT_LINE_REGEX = /^\s*(<!--\s*|)(\[|)((@|)[_a-zA-Z][_a-zA-Z0-9]*)(|\s+([_a-zA-Z0-9]+))(\]|)(\s*-->|)?\s*$/
170
222
 
171
- class ParseException<Exception
223
+ # Any error that occurs when parsing a source file
224
+ class ParseException < Exception
172
225
  end
173
226
 
227
+ # Regenerate delimits page components ("sections") using special HTML comments.
228
+ # The comment lines may - 1. start a comment, 2. end a comment,
229
+ # 3. be a self-contained comment line (in which case the self-contained comment may a) start or b) end a page component,
230
+ # or c) be a page component in itself.
231
+ # The components of a Regenerate comment line are defined by the regex COMMENT_LINE_REGEX, and
232
+ # are identified as follows:
233
+ # * "<!--" Comment start
234
+ # * "[" Section start
235
+ # * Ruby instance variable name (identifier starting with "@"), or,
236
+ # a special section name (currently must be one of "ruby" or "class")
237
+ # * "]" Section end
238
+ # * "-->" Comment end
239
+ # To be identified as a Regenerate comment line, a line must contain at least one of a
240
+ # comment start and a comment end, and at least one of a section start and a section end.
241
+
242
+ # An object which matches the regex used to identify Regenerate comment line commands
243
+ # (Note, however, a parsed line may match the regex, but if it doesn't have at least one of a comment
244
+ # start or a comment end and at least one of a section start or a section end, it will be assumed
245
+ # that it is a line which was not intended to be parsed as a Regenerate command.)
174
246
  class ParsedRegenerateCommentLine
175
247
 
176
- attr_reader :isInstanceVar, :hasCommentStart, :hasCommentEnd, :sectionStart, :sectionEnd
177
- attr_reader :isEmptySection, :line, :name, :value
248
+ attr_reader :line # The full text line matched against
249
+ attr_reader :isInstanceVar # Is there an associated page object instance variable?
250
+ attr_reader :hasCommentStart # Does this command include the start of an HTML comment?
251
+ attr_reader :hasCommentEnd # Does this command include the end of an HTML comment?
252
+ attr_reader :sectionStart # Does this command include a section start indicator, i.e. "[" ?
253
+ attr_reader :sectionEnd # Does this command include a section start indicator, i.e. "]" ?
254
+ attr_reader :isEmptySection # Does this represent an empty section, because it starts and ends the same section?
255
+ attr_reader :name # The instance variable name (@something) or special command name ("ruby" or "class")
256
+ attr_reader :value # The optional value associated with a special command
178
257
 
179
258
  def initialize(line, match)
180
259
  @hasCommentStart = match[1] != ""
@@ -188,40 +267,51 @@ module Regenerate
188
267
  @isEmptySection = @sectionStart && @sectionEnd
189
268
  end
190
269
 
270
+ # Reconstruct a line which would re-parse the same (but possibly with reduced whitespace)
191
271
  def to_s
192
272
  "#{@hasCommentStart ? "<!-- ":""}#{@sectionStart ? "[ ":""}#{@isInstanceVar ? "@ ":""}#{@name.inspect}#{@value ? " "+@value:""}#{@sectionEnd ? " ]":""}#{@hasCommentEnd ? " -->":""}"
193
273
  end
194
274
 
195
- def isRejennerCommentLine
275
+ # Is this line recognised as a Regenerate comment line command?
276
+ def isRegenerateCommentLine
196
277
  return (@hasCommentStart || @hasCommentEnd) && (@sectionStart || @sectionEnd)
197
278
  end
198
279
 
280
+ # Does this command start a Ruby page component (because it has special command name "ruby")?
199
281
  def isRuby
200
282
  !@isInstanceVar && @name == "ruby"
201
283
  end
202
284
 
285
+ # The name of the associated instance variable (assuming there is one)
203
286
  def instanceVarName
204
287
  return @name
205
288
  end
206
289
 
290
+ # Raise a parse exception due to an error within this command line
207
291
  def raiseParseException(message)
208
292
  raise ParseException.new("Error parsing line #{@line.inspect}: #{message}")
209
293
  end
210
294
 
211
- # only call this method if isRejennerCommentLine returns true
295
+ # only call this method if isRegenerateCommentLine returns true - in other words, if it looks like
296
+ # it was intended to be a Regenerate comment line command, check that it is valid.
212
297
  def checkIsValid
298
+ # The "name" value has to be an instance variable name or "ruby" or "class"
213
299
  if !@isInstanceVar and !["ruby", "class"].include?(@name)
214
300
  raiseParseException("Unknown section name #{@name.inspect}")
215
301
  end
302
+ # An empty section has to be a self-contained comment line
216
303
  if @isEmptySection and (!@hasCommentStart && !@hasCommentEnd)
217
304
  raiseParseException("Empty section, but is not a closed comment")
218
305
  end
306
+ # If it's not a section start, it has to be a section end, so there has to be a comment end
219
307
  if !@sectionStart && !@hasCommentEnd
220
308
  raiseParseException("End of section in comment start")
221
309
  end
310
+ # If it's not a section end, it has to be a section start, so there has to be a comment start
222
311
  if !@sectionEnd && !@hasCommentStart
223
312
  raiseParseException("Start of section in comment end")
224
313
  end
314
+ # Empty Ruby page components aren't allowed.
225
315
  if (@sectionStart && @sectionEnd) && isRuby
226
316
  raiseParseException("Empty ruby section")
227
317
  end
@@ -229,14 +319,17 @@ module Regenerate
229
319
 
230
320
  end
231
321
 
232
- class UnexpectedChangeError<Exception
322
+ # When running with "checkNoChanges" flag, raise this error if a change is observed
323
+ class UnexpectedChangeError < Exception
233
324
  end
234
-
325
+
326
+ # A web page which is read from a source file and regenerated to an output file (which
327
+ # may be the same as the source file)
235
328
  class WebPage
236
329
 
237
330
  include Regenerate::Utils
238
331
 
239
- attr_reader :fileName
332
+ attr_reader :fileName # The absolute name of the source file
240
333
 
241
334
  def initialize(fileName)
242
335
  @fileName = fileName
@@ -244,10 +337,22 @@ module Regenerate
244
337
  @currentComponent = nil
245
338
  @componentInstanceVariables = {}
246
339
  initializePageObject(PageObject.new) # default, can be overridden by SetPageObjectClass
340
+ @pageObjectClassNameSpecified = nil # remember name if we have specified a page object class to override the default
247
341
  @rubyComponents = []
248
342
  readFileLines
249
343
  end
250
-
344
+
345
+ # initialise the "page object", which is the object that "owns" the defined instance variables,
346
+ # and the object in whose context the Ruby components are evaluated
347
+ # Three special instance variable values are set - @fileName, @baseDir, @baseFileName,
348
+ # so that they can be accessed, if necessary, by Ruby code in the Ruby code components.
349
+ # (if this is called a second time, it overrides whatever was set the first time)
350
+ # Notes on special instance variables -
351
+ # @fileName and @baseDir are the absolute paths of the source file and it's containing directory.
352
+ # They would be used in Ruby code that looked for other files with names or locations relative to these two.
353
+ # They would generally not be expected to appear in the output content.
354
+ # @baseFileName is the name of the file without any directory path components. In some cases it might be
355
+ # used within output content.
251
356
  def initializePageObject(pageObject)
252
357
  @pageObject = pageObject
253
358
  setPageObjectInstanceVar("@fileName", @fileName)
@@ -256,32 +361,26 @@ module Regenerate
256
361
  @initialInstanceVariables = Set.new(@pageObject.instance_variables)
257
362
  end
258
363
 
364
+ # Get the value of an instance variable of the page object
259
365
  def getPageObjectInstanceVar(varName)
260
366
  @pageObject.instance_variable_get(varName)
261
367
  end
262
368
 
369
+ # Set the value of an instance variable of the page object
263
370
  def setPageObjectInstanceVar(varName, value)
264
371
  puts " setPageObjectInstanceVar, #{varName} = #{value.inspect}"
265
372
  @pageObject.instance_variable_set(varName, value)
266
373
  end
267
374
 
375
+ # Add a Ruby page component to this web page (so that later on it will be executed)
268
376
  def addRubyComponent(rubyComponent)
269
377
  @rubyComponents << rubyComponent
270
378
  end
271
379
 
272
- def setInstanceVarValue(varName, value)
273
- if @initialInstanceVariables.member? varName
274
- raise Exception, "Instance variable #{varName} is a pre-existing instance variable"
275
- end
276
- if @componentInstanceVariables.member? varName
277
- raise Exception, "Instance variable #{varName} is a already defined for a component"
278
- end
279
- instance_variable_set(varName, value)
280
- componentInstanceVariables << varName
281
- end
282
-
380
+ # Add a newly started page component to this page
381
+ # Also process the start comment, unless it was a static HTML component, in which case there is
382
+ # not start comment.
283
383
  def startNewComponent(component, startComment = nil)
284
-
285
384
  component.parentPage = self
286
385
  @currentComponent = component
287
386
  #puts "startNewComponent, @currentComponent = #{@currentComponent.inspect}"
@@ -291,6 +390,8 @@ module Regenerate
291
390
  end
292
391
  end
293
392
 
393
+ # Process a text line, by adding to the current page component, or if there is none, starting a new
394
+ # StaticHtml component.
294
395
  def processTextLine(line, lineNumber)
295
396
  #puts "text: #{line}"
296
397
  if @currentComponent == nil
@@ -299,56 +400,67 @@ module Regenerate
299
400
  @currentComponent.addLine(line)
300
401
  end
301
402
 
403
+ # Get a Ruby class from a normal Ruby class name formatted using "::" separators
302
404
  def classFromString(str)
405
+ # Start with Object, and look up the module one path component at a time
303
406
  str.split('::').inject(Object) do |mod, class_name|
304
407
  mod.const_get(class_name)
305
408
  end
306
409
  end
307
410
 
411
+ # Set the page object to be an object with the specified class name (this can only be done once)
308
412
  def setPageObject(className)
413
+ if @pageObjectClassNameSpecified
414
+ raise ParseException("Page object class name specified more than once")
415
+ end
416
+ @pageObjectClassNameSpecified = className
309
417
  pageObjectClass = classFromString(className)
310
418
  initializePageObject(pageObjectClass.new)
311
419
  end
312
420
 
421
+ # Process a line of source text that has been identified as a Regenerate start and/or end of comment line
313
422
  def processCommandLine(parsedCommandLine, lineNumber)
314
423
  #puts "command: #{parsedCommandLine}"
315
- if @currentComponent && (@currentComponent.is_a? StaticHtml)
424
+ if @currentComponent && (@currentComponent.is_a? StaticHtml) # finish any current static HTML component
316
425
  @currentComponent.finishText
317
426
  @currentComponent = nil
318
427
  end
319
- if @currentComponent
320
- if parsedCommandLine.sectionStart
428
+ if @currentComponent # we are in a page component other than a static HTML component
429
+ if parsedCommandLine.sectionStart # we have already started, so we cannot start again
321
430
  raise ParseException.new("Unexpected section start #{parsedCommandLine} inside component")
322
431
  end
323
- @currentComponent.processEndComment(parsedCommandLine)
432
+ @currentComponent.processEndComment(parsedCommandLine) # so, command must be a command to end the page component
324
433
  @currentComponent = nil
325
- else
326
- if !parsedCommandLine.sectionStart
434
+ else # not in any page component, so we need to start a new one
435
+ if !parsedCommandLine.sectionStart # if it's an end command, error, because there is nothing to end
327
436
  raise ParseException.new("Unexpected section end #{parsedCommandLine}, outside of component")
328
437
  end
329
- if parsedCommandLine.isInstanceVar
330
- if parsedCommandLine.hasCommentEnd
438
+ if parsedCommandLine.isInstanceVar # it's a page component that defines an instance variable value
439
+ if parsedCommandLine.hasCommentEnd # the value will be an HTML value
331
440
  startNewComponent(HtmlVariable.new, parsedCommandLine)
332
- else
441
+ else # the value will be an HTML-commented value
333
442
  startNewComponent(CommentVariable.new, parsedCommandLine)
334
443
  end
335
- else
336
- if parsedCommandLine.name == "ruby"
444
+ else # not an instance var, so it must be a special command
445
+ if parsedCommandLine.name == "ruby" # Ruby page component containing Ruby that will be executed in the
446
+ # context of the page object
337
447
  startNewComponent(RubyCode.new(lineNumber+1), parsedCommandLine)
338
- elsif parsedCommandLine.name == "class"
448
+ elsif parsedCommandLine.name == "class" # Specify Ruby class for the page object
339
449
  startNewComponent(SetPageObjectClass.new(parsedCommandLine.value), parsedCommandLine)
340
- else
450
+ else # not a known special command
341
451
  raise ParseException.new("Unknown section type #{parsedCommandLine.name.inspect}")
342
452
  end
343
453
  end
344
- if @currentComponent.finished
345
- @currentComponent = nil
454
+ if @currentComponent.finished # Did the processing cause the current page component to be finished?
455
+ @currentComponent = nil # clear the current component
346
456
  end
347
457
  end
348
458
 
349
459
  end
350
460
 
351
- def finish
461
+ # Finish the current page component after we are at the end of the source file
462
+ # Anything other than static HTML should be explicitly finished, and if it isn't finished, raise an error.
463
+ def finishAtEndOfSourceFile
352
464
  if @currentComponent
353
465
  if @currentComponent.is_a? StaticHtml
354
466
  @currentComponent.finishText
@@ -359,6 +471,7 @@ module Regenerate
359
471
  end
360
472
  end
361
473
 
474
+ # Report the difference between two strings (that should be the same)
362
475
  def diffReport(newString, oldString)
363
476
  i = 0
364
477
  minLength = [newString.length, oldString.length].min
@@ -372,7 +485,10 @@ module Regenerate
372
485
  "Different from position #{diffPos}: \n #{newString[startPos...newStringEndPos].inspect}\n !=\n #{oldString[startPos...newStringEndPos].inspect}"
373
486
  end
374
487
 
375
- def checkOutputFileUnchanged(outFile, oldFile)
488
+ # Check that a newly created output file has the same contents as another (backup) file containing the old contents
489
+ # If it has changed, actually reset the new file to have ".new" at the end of its name,
490
+ # and rename the backup file to be the output file (in effect reverting the newly written output).
491
+ def checkAndEnsureOutputFileUnchanged(outFile, oldFile)
376
492
  if File.exists? oldFile
377
493
  oldFileContents = File.read(oldFile)
378
494
  newFileContents = File.read(outFile)
@@ -388,6 +504,8 @@ module Regenerate
388
504
  end
389
505
  end
390
506
 
507
+ # Write the output of the page components to the output file (optionally checking that
508
+ # there are no differences between the new output and the existing output.
391
509
  def writeRegeneratedFile(outFile, checkNoChanges)
392
510
  backupFileName = makeBackupFile(outFile)
393
511
  puts "Outputting regenerated page to #{outFile} ..."
@@ -398,46 +516,51 @@ module Regenerate
398
516
  end
399
517
  puts "Finished writing #{outFile}"
400
518
  if checkNoChanges
401
- checkOutputFileUnchanged(outFile, backupFileName)
519
+ checkAndEnsureOutputFileUnchanged(outFile, backupFileName)
402
520
  end
403
521
  end
404
522
 
523
+ # Read in and parse lines from source file
405
524
  def readFileLines
406
525
  puts "Opening #{@fileName} ..."
407
526
  lineNumber = 0
408
527
  File.open(@fileName).each_line do |line|
409
528
  line.chomp!
410
- lineNumber += 1
529
+ lineNumber += 1 # track line numbers for when Ruby code needs to be executed (i.e. to populate stack traces)
411
530
  #puts "line #{lineNumber}: #{line}"
412
531
  commentLineMatch = COMMENT_LINE_REGEX.match(line)
413
- if commentLineMatch
532
+ if commentLineMatch # it matches the Regenerate command line regex (but might not actually be a command ...)
414
533
  parsedCommandLine = ParsedRegenerateCommentLine.new(line, commentLineMatch)
415
534
  #puts "parsedCommandLine = #{parsedCommandLine}"
416
- if parsedCommandLine.isRejennerCommentLine
417
- parsedCommandLine.checkIsValid
418
- processCommandLine(parsedCommandLine, lineNumber)
535
+ if parsedCommandLine.isRegenerateCommentLine # if it is a Regenerate command line
536
+ parsedCommandLine.checkIsValid # check it is valid, and then,
537
+ processCommandLine(parsedCommandLine, lineNumber) # process the command line
419
538
  else
420
- processTextLine(line, lineNumber)
539
+ processTextLine(line, lineNumber) # process a text line which is not a Regenerate command line
421
540
  end
422
541
  else
423
- processTextLine(line, lineNumber)
542
+ processTextLine(line, lineNumber) # process a text line which is not a Regenerate command line
424
543
  end
425
544
  end
426
- finish
545
+ # After processing all source lines, the only unfinished page component permitted is a static HTML component.
546
+ finishAtEndOfSourceFile
427
547
  #puts "Finished reading #{@fileName}."
428
548
  end
429
549
 
550
+ # Regenerate the source file (in-place)
430
551
  def regenerate
431
552
  executeRubyComponents
432
553
  writeRegeneratedFile(@fileName)
433
554
  #display
434
555
  end
435
556
 
557
+ # Regenerate from the source file into the output file
436
558
  def regenerateToOutputFile(outFile, checkNoChanges = false)
437
559
  executeRubyComponents
438
560
  writeRegeneratedFile(outFile, checkNoChanges)
439
561
  end
440
562
 
563
+ # Execute the Ruby components which consist of Ruby code to be evaluated in the context of the page object
441
564
  def executeRubyComponents
442
565
  fileDir = File.dirname(@fileName)
443
566
  puts "Executing ruby components in directory #{fileDir} ..."
@@ -462,10 +585,14 @@ module Regenerate
462
585
  end
463
586
  end
464
587
  end
465
-
588
+
589
+ # The Ruby object contained within a web page. Instance variables defined in the HTML
590
+ # belong to this object, and Ruby code defined in the page is executed in the context of this object.
591
+ # This class is the base class for classes that define particular types of web pages
466
592
  class PageObject
467
593
  include Regenerate::Utils
468
594
 
595
+ # Method to render an ERB template file in the context of this object
469
596
  def erb(templateFileName)
470
597
  @binding = binding
471
598
  File.open(relative_path(templateFileName), "r") do |input|
@@ -475,22 +602,32 @@ module Regenerate
475
602
  result = template.result(@binding)
476
603
  end
477
604
  end
478
-
605
+
606
+ # Method to render an ERB template (defined in-line) in the context of this object
479
607
  def erbFromString(templateString)
480
608
  @binding = binding
481
609
  template = ERB.new(templateString, nil, nil)
482
610
  template.result(@binding)
483
611
  end
484
-
485
612
 
613
+ # Calculate absolute path given path relative to the directory containing the source file for the web page
486
614
  def relative_path(path)
487
615
  File.expand_path(File.join(@baseDir, path.to_str))
488
616
  end
489
617
 
618
+ # Require a Ruby file given a path relative to the web page source file.
490
619
  def require_relative(path)
491
620
  require relative_path(path)
492
621
  end
493
-
622
+
623
+ # Save some of the page object's instance variable values to a file as JSON
624
+ # This method depends on the following defined in the actual page object class:
625
+ # * propertiesToSave instance method, to return an array of symbols
626
+ # * propertiesFileName class method, to return name of properties file as a function of the web page source file name
627
+ # (propertiesFileName is a class method, because it needs to be invoked by other code that _reads_ the properties
628
+ # file when the page object itself does not exist)
629
+ # Example of useage: an index file for a blog needs to read properties of each blog page,
630
+ # where the blog page objects have saved their details into the individual property files.
494
631
  def saveProperties
495
632
  properties = {}
496
633
  for property in propertiesToSave
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: regenerate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-06-10 00:00:00.000000000 Z
12
+ date: 2013-06-11 00:00:00.000000000 Z
13
13
  dependencies: []
14
14
  description: Use to regenerate to write a web page with embedded instance variable
15
15
  definitions and embedded ruby code which executes to regenerate the same web page