codnar 0.1.64

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.
Files changed (80) hide show
  1. data/ChangeLog +165 -0
  2. data/LICENSE +19 -0
  3. data/README.rdoc +32 -0
  4. data/Rakefile +66 -0
  5. data/bin/codnar-split +5 -0
  6. data/bin/codnar-weave +5 -0
  7. data/codnar.html +10945 -0
  8. data/doc/logo.png +0 -0
  9. data/doc/root.html +22 -0
  10. data/doc/story.markdown +180 -0
  11. data/doc/system.markdown +671 -0
  12. data/lib/codnar.rb +41 -0
  13. data/lib/codnar/application.rb +92 -0
  14. data/lib/codnar/cache.rb +61 -0
  15. data/lib/codnar/data/contents.js +113 -0
  16. data/lib/codnar/data/control_chunks.js +44 -0
  17. data/lib/codnar/data/style.css +95 -0
  18. data/lib/codnar/data/sunlight/README.txt +4 -0
  19. data/lib/codnar/data/sunlight/css-min.js +1 -0
  20. data/lib/codnar/data/sunlight/default.css +236 -0
  21. data/lib/codnar/data/sunlight/javascript-min.js +1 -0
  22. data/lib/codnar/data/sunlight/min.js +1 -0
  23. data/lib/codnar/data/sunlight/ruby-min.js +1 -0
  24. data/lib/codnar/data/yui/README.txt +3 -0
  25. data/lib/codnar/data/yui/base.css +132 -0
  26. data/lib/codnar/data/yui/reset.css +142 -0
  27. data/lib/codnar/formatter.rb +180 -0
  28. data/lib/codnar/grouper.rb +28 -0
  29. data/lib/codnar/gvim.rb +132 -0
  30. data/lib/codnar/hash_extensions.rb +41 -0
  31. data/lib/codnar/markdown.rb +47 -0
  32. data/lib/codnar/merger.rb +138 -0
  33. data/lib/codnar/rake.rb +41 -0
  34. data/lib/codnar/rake/split_task.rb +71 -0
  35. data/lib/codnar/rake/weave_task.rb +59 -0
  36. data/lib/codnar/rdoc.rb +9 -0
  37. data/lib/codnar/reader.rb +121 -0
  38. data/lib/codnar/scanner.rb +216 -0
  39. data/lib/codnar/split.rb +58 -0
  40. data/lib/codnar/split_configurations.rb +367 -0
  41. data/lib/codnar/splitter.rb +32 -0
  42. data/lib/codnar/string_extensions.rb +25 -0
  43. data/lib/codnar/sunlight.rb +17 -0
  44. data/lib/codnar/version.rb +8 -0
  45. data/lib/codnar/weave.rb +58 -0
  46. data/lib/codnar/weave_configurations.rb +48 -0
  47. data/lib/codnar/weaver.rb +105 -0
  48. data/lib/codnar/writer.rb +38 -0
  49. data/test/cache_computations.rb +41 -0
  50. data/test/deep_merge.rb +29 -0
  51. data/test/embed_images.rb +12 -0
  52. data/test/expand_markdown.rb +27 -0
  53. data/test/expand_rdoc.rb +20 -0
  54. data/test/format_code_gvim_configurations.rb +55 -0
  55. data/test/format_code_sunlight_configurations.rb +37 -0
  56. data/test/format_comment_configurations.rb +86 -0
  57. data/test/format_lines.rb +72 -0
  58. data/test/group_lines.rb +31 -0
  59. data/test/gvim_highlight_syntax.rb +49 -0
  60. data/test/identify_chunks.rb +32 -0
  61. data/test/lib/test_with_configurations.rb +15 -0
  62. data/test/merge_lines.rb +133 -0
  63. data/test/rake_tasks.rb +38 -0
  64. data/test/read_chunks.rb +110 -0
  65. data/test/run_application.rb +56 -0
  66. data/test/run_split.rb +38 -0
  67. data/test/run_weave.rb +75 -0
  68. data/test/scan_lines.rb +78 -0
  69. data/test/split_chunk_configurations.rb +55 -0
  70. data/test/split_code.rb +109 -0
  71. data/test/split_code_configurations.rb +73 -0
  72. data/test/split_combined_configurations.rb +114 -0
  73. data/test/split_complex_comment_configurations.rb +73 -0
  74. data/test/split_documentation.rb +92 -0
  75. data/test/split_documentation_configurations.rb +97 -0
  76. data/test/split_simple_comment_configurations.rb +50 -0
  77. data/test/sunlight_highlight_syntax.rb +25 -0
  78. data/test/weave_configurations.rb +144 -0
  79. data/test/write_chunks.rb +28 -0
  80. metadata +363 -0
@@ -0,0 +1,41 @@
1
+ require "rake"
2
+ require "rake/tasklib"
3
+
4
+ require "codnar"
5
+ require "codnar/rake/split_task"
6
+ require "codnar/rake/weave_task"
7
+
8
+ module Codnar
9
+
10
+ # This module contains all the Codnar Rake tasks code.
11
+ module Rake
12
+
13
+ class << self
14
+
15
+ # The root folder to store all chunk files under.
16
+ attr_accessor :chunks_dir
17
+
18
+ # The list of split chunk files for later weaving.
19
+ attr_accessor :chunk_files
20
+
21
+ end
22
+
23
+ Rake.chunk_files = []
24
+ Rake.chunks_dir = "chunks"
25
+
26
+ # Compute options for invoking an application.
27
+ def self.application_options(output, configurations)
28
+ options = [ "-o", output ]
29
+ options += configurations.map { |configuration| [ "-c", configuration.to_s ] }.flatten
30
+ return options
31
+ end
32
+
33
+ # Return the list of actual configuration files (as opposed to names of
34
+ # built-in configurations) for use as dependencies.
35
+ def self.configuration_files(configurations)
36
+ return configurations.find_all { |configuration| File.exists?(configuration.to_s) }
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,71 @@
1
+ module Codnar
2
+
3
+ module Rake
4
+
5
+ # A Rake task for splitting source files to chunks.
6
+ class SplitTask < ::Rake::TaskLib
7
+
8
+ # Create a new Rake task for splitting source files to chunks. Each of
9
+ # the specified disk files is split using the specified set of
10
+ # configurations.
11
+ def initialize(paths, configurations)
12
+ @configurations = configurations
13
+ paths.each do |path|
14
+ define_tasks(path)
15
+ end
16
+ end
17
+
18
+ protected
19
+
20
+ # Define the tasks for splitting a single source file to chunks.
21
+ def define_tasks(path)
22
+ output = Rake.chunks_dir + "/" + path
23
+ define_split_file_task(path, output)
24
+ SplitTask.define_common_tasks
25
+ SplitTask.connect_common_tasks(output)
26
+ end
27
+
28
+ # Define the actual task for splitting the source file.
29
+ def define_split_file_task(path, output)
30
+ ::Rake::FileTask.define_task(output => [ path ] + Rake.configuration_files(@configurations)) do
31
+ run_split_application(path, output)
32
+ end
33
+ end
34
+
35
+ # Run the Split application for a single source file.
36
+ def run_split_application(path, output)
37
+ options = Rake.application_options(output, @configurations)
38
+ options << path
39
+ status = Application.with_argv(options) { Split.new.run }
40
+ raise "Codnar split errors" unless status == 0
41
+ end
42
+
43
+ # Define common Rake split tasks. This method may be invoked several
44
+ # times, only the first invocation actually defined the tasks. The common
45
+ # tasks are codnar_split (for splitting all the source files) and
46
+ # clean_codnar (for getting rid of the chunks directory).
47
+ def self.define_common_tasks
48
+ @defined_common_tasks ||= SplitTask.create_common_tasks
49
+ end
50
+
51
+ # Actually create common Rake split tasks.
52
+ def self.create_common_tasks
53
+ desc "Split all files into chunks"
54
+ ::Rake::Task.define_task("codnar_split")
55
+ desc "Clean all split chunks"
56
+ ::Rake::Task.define_task("clean_codnar") { rm_rf(Rake.chunks_dir) }
57
+ ::Rake::Task.define_task(:clean => "clean_codnar")
58
+ end
59
+
60
+ # Connect the task for splitting a single source file to the common task
61
+ # of splitting all source files.
62
+ def self.connect_common_tasks(output)
63
+ ::Rake::Task.define_task("codnar_split" => output)
64
+ Rake::chunk_files << output
65
+ end
66
+
67
+ end
68
+
69
+ end
70
+
71
+ end
@@ -0,0 +1,59 @@
1
+ module Codnar
2
+
3
+ module Rake
4
+
5
+ # A Rake task for weaving chunks to a single HTML.
6
+ class WeaveTask < ::Rake::TaskLib
7
+
8
+ # Create a Rake task for weaving chunks to a single HTML. The root source
9
+ # file is expected to embed all the chunks into the output HTML. The
10
+ # chunks are loaded from the results of all the previous created
11
+ # SplitTask-s.
12
+ def initialize(root, configurations, output = "codnar.html")
13
+ @root = Rake.chunks_dir + "/" + root
14
+ @output = output
15
+ @configurations = configurations
16
+ define_tasks
17
+ end
18
+
19
+ protected
20
+
21
+ # Define the tasks for weaving the chunks to a single HTML.
22
+ def define_tasks
23
+ define_weave_task
24
+ connect_common_tasks
25
+ end
26
+
27
+ # Define the actual task for weaving the chunks to a single HTML.
28
+ def define_weave_task
29
+ desc "Weave chunks into HTML" unless ::Rake.application.last_comment
30
+ ::Rake::Task.define_task("codnar_weave" => @output)
31
+ ::Rake::FileTask.define_task(@output => Rake.chunk_files + Rake.configuration_files(@configurations)) do
32
+ run_weave_application
33
+ end
34
+ end
35
+
36
+ # Run the Weave application for a single source file.
37
+ def run_weave_application
38
+ options = Rake.application_options(@output, @configurations)
39
+ options << @root
40
+ options += Rake.chunk_files.reject { |chunk| chunk == @root }
41
+ status = Application.with_argv(options) { Weave.new.run }
42
+ raise "Codnar weave errors" unless status == 0
43
+ end
44
+
45
+ # Connect the task for cleaning up after weaving (+clobber_codnar+) to the
46
+ # common task of cleaning up everything (+clobber+).
47
+ def connect_common_tasks
48
+ desc "Build the code narrative HTML"
49
+ ::Rake::Task.define_task(:codnar => "codnar_weave")
50
+ desc "Remove woven HTML documentation"
51
+ ::Rake::Task.define_task("clobber_codnar") { rm_rf(@output) }
52
+ ::Rake::Task.define_task(:clobber => "clobber_codnar")
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,9 @@
1
+ # Extend the RDoc module.
2
+ module RDoc
3
+
4
+ # Process a RDoc String and return the resulting HTML.
5
+ def self.to_html(rdoc)
6
+ return ::RDoc::Markup::ToHtml.new.convert(rdoc).clean_markup_html
7
+ end
8
+
9
+ end
@@ -0,0 +1,121 @@
1
+ module Codnar
2
+
3
+ # Read chunks from disk files.
4
+ class Reader
5
+
6
+ # Load all chunks from the specified disk files to memory for later access
7
+ # by name.
8
+ def initialize(errors, paths)
9
+ @errors = errors
10
+ @chunks = {}
11
+ @used = {}
12
+ paths.each do |path|
13
+ read_path_chunks(path)
14
+ end
15
+ end
16
+
17
+ # Fetch a chunk by its name.
18
+ def [](name)
19
+ id = name.to_id
20
+ @used[id] = true
21
+ return @chunks[id] ||= (
22
+ @errors << "Missing chunk: #{name}"
23
+ Reader.fake_chunk(name)
24
+ )
25
+ end
26
+
27
+ # Collect errors for unused chunks.
28
+ def collect_unused_chunk_errors
29
+ @chunks.each do |id, chunk|
30
+ @errors.push("#{$0}: Unused chunk: #{chunk.name} #{Reader.locations_message(chunk)}") unless @used[id]
31
+ end
32
+ end
33
+
34
+ protected
35
+
36
+ # Load and merge all chunks from a disk file into memory.
37
+ def read_path_chunks(path)
38
+ @errors.in_path(path) do
39
+ chunks = load_path_chunks(path)
40
+ next unless chunks
41
+ merge_loaded_chunks(chunks)
42
+ @root_chunk ||= chunks[0].name
43
+ end
44
+ end
45
+
46
+ # Load all chunks from a disk file into memory.
47
+ def load_path_chunks(path)
48
+ chunks = YAML.load_file(path)
49
+ @errors << "Invalid chunks data" unless chunks
50
+ # TODO: A bit more validation would be nice.
51
+ return chunks
52
+ end
53
+
54
+ # Merge an array of chunks into memory.
55
+ def merge_loaded_chunks(chunks)
56
+ chunks.each do |new_chunk|
57
+ old_chunk = @chunks[id = new_chunk.name.to_id]
58
+ if old_chunk.nil?
59
+ @chunks[id] = new_chunk
60
+ elsif Reader.same_chunk?(old_chunk, new_chunk)
61
+ Reader.merge_same_chunks(old_chunk, new_chunk)
62
+ else
63
+ @errors.push(Reader.different_chunks_error(old_chunk, new_chunk))
64
+ end
65
+ end
66
+ end
67
+
68
+ # Merge a new "same" chunk into an old one.
69
+ def self.merge_same_chunks(old_chunk, new_chunk)
70
+ old_chunk.locations = \
71
+ (old_chunk.locations + new_chunk.locations).uniq.sort \
72
+ do |first_location, second_location|
73
+ [ first_location.file.to_id, first_location.line ] \
74
+ <=> [ second_location.file.to_id, second_location.line ]
75
+ end
76
+ old_chunk.containers = \
77
+ (old_chunk.containers + new_chunk.containers).uniq.sort \
78
+ do |first_name, second_name|
79
+ first_name.to_id <=> second_name.to_id
80
+ end
81
+ end
82
+
83
+ # Check whether two chunks contain the same "stuff".
84
+ def self.same_chunk?(old_chunk, new_chunk)
85
+ return Reader.chunk_payload(old_chunk) == Reader.chunk_payload(new_chunk)
86
+ end
87
+
88
+ # Return just the actual payload of a chunk for equality comparison.
89
+ def self.chunk_payload(chunk)
90
+ chunk = chunk.reject { |key, value| [ "locations", "name", "containers" ].include?(key) }
91
+ chunk.contained.map! { |name| name.to_id }
92
+ return chunk
93
+ end
94
+
95
+ # Error message when two different chunks have the same name.
96
+ def self.different_chunks_error(old_chunk, new_chunk)
97
+ old_location = Reader.locations_message(old_chunk)
98
+ new_location = Reader.locations_message(new_chunk)
99
+ return "#{$0}: Chunk: #{old_chunk.name} is different #{new_location}, and #{old_location}"
100
+ end
101
+
102
+ # Format a chunk's location for an error message.
103
+ def self.locations_message(chunk)
104
+ locations = chunk.locations.map { |location| "in file: #{location.file} at line: #{location.line}" }
105
+ return locations.join(" or ")
106
+ end
107
+
108
+ # Return a fake chunk for the specified name.
109
+ def self.fake_chunk(name)
110
+ return {
111
+ "name" => name,
112
+ "locations" => [ { "file" => "MISSING" } ],
113
+ "contained" => [],
114
+ "containers" => [],
115
+ "html" => "<div class='missing chunk error'>\nMISSING\n</div>"
116
+ }
117
+ end
118
+
119
+ end
120
+
121
+ end
@@ -0,0 +1,216 @@
1
+ module Codnar
2
+
3
+ # Scan a file into classified lines.
4
+ class Scanner
5
+
6
+ # Construct a scanner based on a syntax in the following structure:
7
+ #
8
+ # patterns:
9
+ # <name>:
10
+ # name: <name>
11
+ # kind: <kind>
12
+ # regexp: <regexp>
13
+ # groups:
14
+ # - <name>
15
+ # states:
16
+ # <name>:
17
+ # name: <name>
18
+ # transitions:
19
+ # - pattern: <pattern>
20
+ # kind: <kind>
21
+ # next_state: <state>
22
+ # start_state: <state>
23
+ #
24
+ # To allow for cleaner YAML files to specify the syntax, the following
25
+ # shorthands are supported:
26
+ #
27
+ # - A pattern or state reference can be presented by the string name of the
28
+ # pattern or state.
29
+ # - The name field of a state or pattern can be ommitted. If specified, it
30
+ # must be identical to the key in the states or patterns mapping.
31
+ # - The kind field of a pattern can be ommitted; by default it is assumed
32
+ # to be identical to the pattern name.
33
+ # - A pattern regexp can be presented by a plain string.
34
+ # - The pattern groups field can be ommitted or contain +nil+ if it is
35
+ # equal to [ "indentation", "payload" ].
36
+ # - The kind field of a transition can be ommitted; by default it is
37
+ # assumed to be identical to the pattern kind.
38
+ # - The next state of a transition can be ommitted; by default it is
39
+ # assumed to be identical to the containing state.
40
+ # - The start state can be ommitted; by default it is assumed to be named
41
+ # +start+.
42
+ #
43
+ # When the Scanner is constructed, a deep clone of the syntax object is
44
+ # created and modified to expand all the above shorthands. Any problems
45
+ # detected during this process are pushed into the errors.
46
+ def initialize(errors, syntax)
47
+ @errors = errors
48
+ @syntax = syntax.deep_clone
49
+ @syntax.patterns.each { |name, pattern| expand_pattern_shorthands(name, pattern) }
50
+ @syntax.states.each { |name, state| expand_state_shorthands(name, state) }
51
+ @syntax.start_state = resolve_start_state
52
+ end
53
+
54
+ # Scan a disk file into classified lines in the following format (where the
55
+ # groups contain the text extracted by the matching pattern):
56
+ #
57
+ # - kind: <kind>
58
+ # line: <text>
59
+ # <group>: <text>
60
+ #
61
+ # By convention, each classified line has a "payload" group that contains
62
+ # the "main" content of the line (chunk name for begin/end/nested chunk
63
+ # lines, clean comment text for comment lines, etc.). In addition, most
64
+ # classified lines have an "indentation" group that contains the leading
65
+ # white space (which is not included in the payload).
66
+ #
67
+ # If at some state, a file line does not match any pattern, the scanner
68
+ # will push a message into the errors. In addition it will classify the
69
+ # line as follows:
70
+ #
71
+ # - kind: error
72
+ # state: <name>
73
+ # line: <text>
74
+ # indentation: <leading white space>
75
+ # payload: <line text following the indentation>
76
+ def lines(path)
77
+ @path = path
78
+ @lines = []
79
+ @state = @syntax.start_state
80
+ @errors.in_path(path) { scan_path }
81
+ return @lines
82
+ end
83
+
84
+ protected
85
+
86
+ # {{{ Scanner pattern shorthands
87
+
88
+ # Expand all the shorthands used in the pattern.
89
+ def expand_pattern_shorthands(name, pattern)
90
+ pattern.kind ||= fill_name(name, pattern, "Pattern")
91
+ pattern.groups ||= [ "indentation", "payload" ]
92
+ pattern.regexp = convert_to_regexp(name, pattern.regexp)
93
+ end
94
+
95
+ # Convert a string regexp to a real Regexp.
96
+ def convert_to_regexp(name, regexp)
97
+ return regexp if Regexp == regexp
98
+ begin
99
+ return Regexp.new(regexp)
100
+ rescue
101
+ @errors << "Invalid pattern: #{name} regexp: #{regexp} error: #{$!}"
102
+ end
103
+ end
104
+
105
+ # Fill in the name field for state or pattern object.
106
+ def fill_name(name, data, type)
107
+ data_name = data.name ||= name
108
+ @errors << "#{type}: #{name} has wrong name: #{data_name}" if data_name != name
109
+ return data_name
110
+ end
111
+
112
+ # }}}
113
+
114
+ # {{{ Scanner state shorthands
115
+
116
+ # Expand all the shorthands used in the state.
117
+ def expand_state_shorthands(name, state)
118
+ fill_name(name, state, "State")
119
+ state.transitions.each do |transition|
120
+ pattern = transition.pattern = lookup(@syntax.patterns, "pattern", transition.pattern)
121
+ transition.kind ||= pattern.andand.kind
122
+ transition.next_state = lookup(@syntax.states, "state", transition.next_state || state)
123
+ end
124
+ end
125
+
126
+ # Convert a string name to an actual data reference.
127
+ def lookup(mapping, type, reference)
128
+ return reference unless String === reference
129
+ data = mapping[reference]
130
+ @errors << "Reference to a missing #{type}: #{reference}" unless data
131
+ return data
132
+ end
133
+
134
+ # Resolve the start state reference.
135
+ def resolve_start_state
136
+ return lookup(@syntax.states, "state", @syntax.start_state || "start") || {
137
+ "name" => "missing_start_state",
138
+ "kind" => "error",
139
+ "transitions" => []
140
+ }
141
+ end
142
+
143
+ # }}}
144
+
145
+ # {{{ Scanner file processing
146
+
147
+ # Scan a disk file.
148
+ def scan_path
149
+ File.open(@path, "r") do |file|
150
+ scan_file(file)
151
+ end
152
+ end
153
+
154
+ # Scan an opened file.
155
+ def scan_file(file)
156
+ @line_number = 0
157
+ file.read.each_line do |line|
158
+ @errors.at_line(@line_number += 1)
159
+ scan_line(line.chomp)
160
+ end
161
+ end
162
+
163
+ # Scan the next file line.
164
+ def scan_line(line)
165
+ @state.transitions.each do |transition|
166
+ return if transition.pattern && transition.next_state && classify_matching_line(line, transition)
167
+ end
168
+ unclassified_line(line, @state.name)
169
+ end
170
+
171
+ # }}}
172
+
173
+ # {{{ Scanner line processing
174
+
175
+ # Handle a file line, only if it matches the pattern.
176
+ def classify_matching_line(line, transition)
177
+ match = (pattern = transition.pattern).regexp.match(line)
178
+ return false unless match
179
+ @lines << Scanner.extracted_groups(match, pattern.groups).update({
180
+ "line" => line,
181
+ "kind" => transition.kind,
182
+ "number" => @line_number
183
+ })
184
+ @state = transition.next_state
185
+ return true
186
+ end
187
+
188
+ # Extract named groups from a match. As a special case, indentation is
189
+ # deleted if there is no payload.
190
+ def self.extracted_groups(match, groups)
191
+ extracted = {}
192
+ groups.each_with_index do |group, index|
193
+ extracted[group] = match[index + 1]
194
+ end
195
+ extracted.delete("indentation") if match[0] == ""
196
+ return extracted
197
+ end
198
+
199
+ # Handle a file line that couldn't be classified.
200
+ def unclassified_line(line, state_name)
201
+ @lines << {
202
+ "line" => line,
203
+ "indentation" => line.indentation,
204
+ "payload" => line.unindent,
205
+ "kind" => "error",
206
+ "state" => state_name,
207
+ "number" => @line_number
208
+ }
209
+ @errors << "State: #{state_name} failed to classify line: #{@lines.last.payload}"
210
+ end
211
+
212
+ # }}}
213
+
214
+ end
215
+
216
+ end