regenerate 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/regenerate.rb +2 -1
- data/lib/regenerate/regenerate-utils.rb +4 -0
- data/lib/regenerate/site-regenerator.rb +31 -3
- data/lib/regenerate/web-page.rb +211 -74
- metadata +2 -2
data/lib/regenerate.rb
CHANGED
@@ -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
|
-
|
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)
|
data/lib/regenerate/web-page.rb
CHANGED
@@ -5,74 +5,93 @@ require 'regenerate/regenerate-utils.rb'
|
|
5
5
|
|
6
6
|
module Regenerate
|
7
7
|
|
8
|
-
#
|
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
|
-
|
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
|
-
|
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
|
90
|
-
|
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
|
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
|
-
#
|
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
|
-
#
|
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
|
-
|
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
|
-
|
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 :
|
177
|
-
attr_reader :
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
273
|
-
|
274
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
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.
|
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-
|
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
|