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.
- 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
|