regenerate 0.1.1 → 0.2.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.
@@ -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