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,180 @@
1
+ module Codnar
2
+
3
+ # Format chunks into HTML.
4
+ class Formatter
5
+
6
+ # Construct a Formatter based on a mapping from a classified line kind, to
7
+ # a Ruby expression, that converts an array of classified lines of that
8
+ # kind, into an array of lines of another kind. This expression is simply
9
+ # +eval+-ed, and is expected to make use of a variable called +lines+ that
10
+ # contains an array of classified lines, as produced by a Scanner. The
11
+ # result of evaluating the expressions is expected to be an array of any
12
+ # number of classified lines of any kind.
13
+ #
14
+ # Formatting repeatedly applies these formatting expressions, until the
15
+ # result is an array containing a single classified line, which has the
16
+ # kind +html+ and whose payload field contains the unified final HTML
17
+ # presentation of the original classified lines. In each processing round,
18
+ # all consecutive lines of the same kind are formated together. This allows
19
+ # for properly formating line kinds that use a multi-line notation such as
20
+ # Markdown.
21
+ #
22
+ # The default formatting expression for the kind +html+ simply joins all
23
+ # the payloads of all the classified lines into a single html, and returns
24
+ # a single "line" containing this joined HTML. All other line kinds need to
25
+ # have a formatting expression explicitly specified in the formatters
26
+ # mapping.
27
+ #
28
+ # If no formatting expression is specified for some classified line kind,
29
+ # an error is reported and the classified lines are wrapped in a pre HTML
30
+ # element with a +missing_formatter+ CSS class. Similarly, if a formatting
31
+ # expression fails (raises an exception), an error is reported and the
32
+ # lines are wrapped in a pre HTML element with a +failed_formatter+ CSS
33
+ # class.
34
+ def initialize(errors, formatters)
35
+ @errors = errors
36
+ @formatters = { "html" => "Formatter.merge_html_lines(lines)" }.merge(formatters)
37
+ end
38
+
39
+ # Repeatedly process an array of classified lines of arbitrary kinds until
40
+ # we obtain a single classified "line" containing a unified final HTML
41
+ # presentation of the original classified lines.
42
+ def lines_to_html(lines)
43
+ until Formatter.single_html_line?(lines)
44
+ lines = Grouper.lines_to_groups(lines).map { |group| process_lines_group(group) }.flatten
45
+ end
46
+ return lines.last.andand.payload.to_s
47
+ end
48
+
49
+ protected
50
+
51
+ # Check whether we have finally got a single HTML classified "line" for the
52
+ # whole classified lines sequence.
53
+ def self.single_html_line?(lines)
54
+ return lines.size <= 1 && lines[0].andand.kind == "html"
55
+ end
56
+
57
+ # Perform one pass of processing toward HTML on a group of consecutive
58
+ # classified lines with the same kind.
59
+ def process_lines_group(lines)
60
+ kind = lines.last.kind
61
+ formatter = @formatters[kind] ||= missing_formatter(kind)
62
+ begin
63
+ return eval formatter
64
+ rescue
65
+ return failed_formatter(lines, formatter, $!)
66
+ end
67
+ end
68
+
69
+ # Return an expression for formatting classified lines of some kind that
70
+ # doesn't have such a formatting expression already specified.
71
+ def missing_formatter(kind)
72
+ @errors << "No formatter specified for lines of kind: #{kind}"
73
+ return "Formatter.lines_to_pre_html(lines, :class => 'missing formatter error')"
74
+ end
75
+
76
+ # Format classified lines as HTML if the original specified formatting
77
+ # expression failed.
78
+ def failed_formatter(lines, formatter, exception)
79
+ @errors << "Formatter: #{formatter} for lines of kind: #{lines.last.kind} failed with exception: #{exception}"
80
+ return Formatter.lines_to_pre_html(lines, :class => "failed formatter error")
81
+ end
82
+
83
+ # {{{ Basic formatters
84
+
85
+ # Merge a group of consecutive indented lines into a group with a single
86
+ # classified "line". The given block is passed the joined content of all
87
+ # the lines, and may process it to yield the merged "line" content. If an
88
+ # explicit indentation is given, it overrides each line's indentation. This
89
+ # is useful for avoiding the inclusion of the indentation in the payload.
90
+ def self.merge_lines(lines, kind, indentation = nil)
91
+ payload = yield lines.map { |line| (indentation || line.indentation || "") + (line.payload || "") }.join("\n")
92
+ merged_line = lines[0]
93
+ merged_line.merge!("kind" => kind, "payload" => payload)
94
+ merged_line.delete("indentation") if indentation.nil?
95
+ return [ merged_line ]
96
+ end
97
+
98
+ # Merge a group of consecutive HTML classified lines into a group with a
99
+ # single HTML classified "line". This is the default formatting expression
100
+ # for HTML lines.
101
+ def self.merge_html_lines(lines)
102
+ return Formatter.merge_lines(lines, "html") { |payload| payload }
103
+ end
104
+
105
+ # Format classified lines into HTML using a pre element with optional
106
+ # attributes. This is the default formatting expression for classified
107
+ # lines of unknown kinds.
108
+ def self.lines_to_pre_html(lines, attributes = {})
109
+ return Formatter.merge_lines(lines, "html") do |payload|
110
+ ( "<pre" + Formatter.html_attributes(attributes) + ">\n" \
111
+ + CGI.escapeHTML(payload) + "\n" \
112
+ + "</pre>" )
113
+ end
114
+ end
115
+
116
+ # Convert an attribute mapping to HTML.
117
+ def self.html_attributes(attributes)
118
+ return "" if attributes == {}
119
+ return " " + attributes.map { |name, value| "#{name}='#{CGI.escapeHTML(value.to_s)}'" }.join(" ")
120
+ end
121
+
122
+ # Format classified lines that indicate a nested chunk to HTML.
123
+ def self.nested_chunk_lines_to_html(lines)
124
+ return lines.each do |line|
125
+ line.kind = "html"
126
+ chunk_name = line.payload
127
+ line.payload = "<pre class='nested chunk'>\n" \
128
+ + (line.indentation || "") \
129
+ + "<a class='nested chunk' href='##{chunk_name.to_id}'>#{CGI.escapeHTML(chunk_name)}</a>\n" \
130
+ + "</pre>"
131
+ line.delete("indentation")
132
+ end
133
+ end
134
+
135
+ # Indent arbitrary HTML lines to line up with the rest of the lines.
136
+ def self.unindented_lines_to_html(lines)
137
+ merged_line = lines[0]
138
+ html = lines.map { |line| line.payload + "\n" }.join
139
+ merged_line.payload = self.indent_html(merged_line.indentation, html)
140
+ merged_line.kind = "html"
141
+ return [ merged_line ]
142
+ end
143
+
144
+ # Indent a chunk of HTML by some spaces. This uses a table, which is
145
+ # arguably the wrong way to do it.
146
+ def self.indent_html(indentation, html)
147
+ return html.chomp if indentation.nil?
148
+ return "<table class='layout'>\n<tr>\n" \
149
+ + "<td class='indentation'>\n" \
150
+ + "<pre>#{indentation}</pre>\n" \
151
+ + "</td>\n" \
152
+ + "<td class='html'>\n" \
153
+ + html \
154
+ + "</td>\n" \
155
+ + "</tr>\n</table>"
156
+ end
157
+
158
+ # Cast a sequence of classified lines into a different kind without
159
+ # any processing.
160
+ def self.cast_lines(lines, kind)
161
+ lines = lines.dup
162
+ lines.each { |line| line.kind = kind }
163
+ return lines
164
+ end
165
+
166
+ # Convert a sequence of marked-up classified lines to (unindented) HTML
167
+ def self.markup_lines_to_html(lines, klass)
168
+ implementation = String === klass ? Kernel.const_get(klass) : klass
169
+ return Formatter.merge_lines(lines, "unindented_html", "") do |payload|
170
+ ( "<div class='#{klass.downcase} #{lines[0].kind} markup'>\n" \
171
+ + implementation.to_html(payload) \
172
+ + "</div>" )
173
+ end
174
+ end
175
+
176
+ # }}}
177
+
178
+ end
179
+
180
+ end
@@ -0,0 +1,28 @@
1
+ module Codnar
2
+
3
+ # Group classified lines according to kind.
4
+ module Grouper
5
+
6
+ # Convert array of classified lines to array of classified line groups with
7
+ # the same line kind.
8
+ def self.lines_to_groups(lines)
9
+ groups = lines.reduce([], &method(:group_next_line))
10
+ return groups
11
+ end
12
+
13
+ protected
14
+
15
+ # Add the next classified line to the classified line groups.
16
+ def self.group_next_line(groups, next_line)
17
+ last_group = groups.last
18
+ if last_group.andand.last.andand.kind == next_line.kind
19
+ last_group.push(next_line)
20
+ else
21
+ groups.push([ next_line ])
22
+ end
23
+ return groups
24
+ end
25
+
26
+ end
27
+
28
+ end
@@ -0,0 +1,132 @@
1
+ module Codnar
2
+
3
+ # Syntax highlight using GVim.
4
+ class GVim
5
+
6
+ # Convert a sequence of classified code lines to HTML using GVim syntax
7
+ # highlighting. The commands array allows configuring the way that GVim
8
+ # will format the output (see the +cached_syntax_to_html+ method for
9
+ # details).
10
+ def self.lines_to_html(lines, syntax, commands = [])
11
+ return Formatter.merge_lines(lines, "html") do |payload|
12
+ GVim.cached_syntax_to_html(payload + "\n", syntax, commands).chomp
13
+ end
14
+ end
15
+
16
+ # The cache used for speeding up recomputing the same syntax highlighting
17
+ # HTML.
18
+ @cache = Cache.new(".gvim-cache") do |data|
19
+ GVim.uncached_syntax_to_html(data.text, data.syntax, data.commands)
20
+ end
21
+
22
+ # Force recomputation of the syntax highlighting HTML, even if a cached
23
+ # version exists.
24
+ def self.force_recompute=(force_recompute)
25
+ @cache.force_recompute = force_recompute
26
+ end
27
+
28
+ # Highlight syntax of text using GVim. This uses the GVim standard CSS
29
+ # classes to mark keywords, identifiers, and so on. See the GVim
30
+ # documentation for details. The commands array allows configuring the way
31
+ # that GVim will format the output. For example:
32
+ #
33
+ # * The command <tt>"+:colorscheme <name>"</tt> will override the default
34
+ # color scheme used.
35
+ # * The command <tt>"+:let html_use_css=1"</tt> will just annotate each
36
+ # HTML tag with a CSS class, instead of embedding some specific style
37
+ # directly into the tag. In this case the colorscheme and background are
38
+ # ignored; you will need to provide your own CSS stylesheet as part of
39
+ # the final woven document to style the marked-up words.
40
+ #
41
+ # Additional commands may be useful; GVim provides a full scripting
42
+ # environment so there is no theoretical limit to what can be done here.
43
+ #
44
+ # Since GVim is as slow as molasses to start up, we cache the results of
45
+ # highlighting the syntax of each code fragment in a directory called
46
+ # <tt>.gvim-cache</tt>, which can appear at the current working directory
47
+ # or in any of its parents.
48
+ def self.cached_syntax_to_html(text, syntax, commands = [])
49
+ data = { "text" => text, "syntax" => syntax, "commands" => commands }
50
+ return @cache[data]
51
+ end
52
+
53
+ # Highlight syntax of text using GVim, without caching. This is *slow*
54
+ # (measured in seconds), due to GVim's start-up tim. See the
55
+ # +cached_syntax_to_html+ method for a faster variant and functionality
56
+ # details.
57
+ def self.uncached_syntax_to_html(text, syntax, commands = [])
58
+ file = write_temporary_file(text)
59
+ run_gvim(file, syntax, commands)
60
+ html = read_html_file(file)
61
+ delete_temporary_files(file)
62
+ return clean_html(html, syntax)
63
+ end
64
+
65
+ protected
66
+
67
+ # Write the text to highlight the syntax of into a temporary file.
68
+ def self.write_temporary_file(text)
69
+ file = Tempfile.open("codnar-")
70
+ file.write(text)
71
+ file.close(false)
72
+ return file
73
+ end
74
+
75
+ # Run GVim to highlight the syntax of a temporary file. This uses the
76
+ # little-known ability of GVim to emit the syntax highlighting as HTML
77
+ # using only command-line arguments.
78
+ def self.run_gvim(file, syntax, commands)
79
+ path = file.path
80
+ ENV["DISPLAY"] = "none" # Otherwise the X11 server *does* affect the result.
81
+ command = [
82
+ "gvim",
83
+ "-f", "-X",
84
+ "-u", "none",
85
+ "-U", "none",
86
+ "+:let html_ignore_folding=1",
87
+ "+:let use_xhtml=1",
88
+ "+:let html_use_css=0",
89
+ "+:syn on",
90
+ "+:set syntax=#{syntax}",
91
+ commands,
92
+ "+run! syntax/2html.vim",
93
+ "+:f #{path}",
94
+ "+:wq", "+:q",
95
+ path
96
+ ]
97
+ system("echo '\n' | '#{command.flatten.join("' '")}' > /dev/null 2>&1")
98
+ end
99
+
100
+ # Read the HTML with the syntax highlighting written out by GVim.
101
+ def self.read_html_file(file)
102
+ return File.read(html_file_path(file))
103
+ end
104
+
105
+ # Delete both the text and HTML temporary files.
106
+ def self.delete_temporary_files(file)
107
+ File.delete(html_file_path(file))
108
+ file.delete
109
+ end
110
+
111
+ # Find the path of the generate HTML file. You'd think it would be
112
+ # predictable, but it ends up either ".html" or ".xhtml" depending on the
113
+ # system.
114
+ def self.html_file_path(file)
115
+ return Dir.glob(file.path + ".*html")[0]
116
+ end
117
+
118
+ # Extract the clean highlighted syntax HTML from GVim's HTML output.
119
+ def self.clean_html(html, syntax)
120
+ if html =~ /<pre>/
121
+ html.sub!(/.*?<pre>/m, "<pre class='#{syntax} code syntax'>")
122
+ html.sub!("</body>\n</html>\n", "")
123
+ else
124
+ html.sub!(/.*?<body/m, "<div class='#{syntax} code syntax'")
125
+ html.sub!("</body>\n</html>\n", "</div>\n")
126
+ end
127
+ return html
128
+ end
129
+
130
+ end
131
+
132
+ end
@@ -0,0 +1,41 @@
1
+ # Extend the core Hash class.
2
+ class Hash
3
+
4
+ # Obtain a deep clone which shares nothing with this hash.
5
+ def deep_clone
6
+ return YAML.load(to_yaml)
7
+ end
8
+
9
+ # {{{ Deep merge
10
+
11
+ # Perform a deep merge with another hash.
12
+ def deep_merge(hash)
13
+ return merge(hash, &Hash::method("deep_merger"))
14
+ end
15
+
16
+ protected
17
+
18
+ # Return a Hash merger that recursively merges nested hashes.
19
+ def self.deep_merger(key, default, override)
20
+ if Hash === default && Hash === override
21
+ default.deep_merge(override)
22
+ elsif Array === default && Array === override
23
+ Hash.deep_merge_arrays(default, override)
24
+ else
25
+ override
26
+ end
27
+ end
28
+
29
+ # If the overriding data array contains an empty array element ("[]"), it is
30
+ # replaced by the default data array being overriden.
31
+ def self.deep_merge_arrays(default, override)
32
+ embed_index = override.find_index([])
33
+ return override unless embed_index
34
+ override = override.dup
35
+ override[embed_index..embed_index] = default
36
+ return override
37
+ end
38
+
39
+ # }}}
40
+
41
+ end
@@ -0,0 +1,47 @@
1
+ # Extend the Markdown class.
2
+ class Markdown
3
+
4
+ # Process a Markdown String and return the resulting HTML. In addition to the
5
+ # normal Markdown syntax, processing supports the following Codnar-specific
6
+ # extensions:
7
+ #
8
+ # * The notation "[[chunk|template]]" is expanded to embedding the specified
9
+ # chunk (name) using the specified template at Weave time.
10
+ # * The notation "[[#name]]" defines an empty anchor. The HTML anchor id is
11
+ # not the specified name, but rather the identifier generated from it (in
12
+ # the same way that chunk names are converted to identifiers).
13
+ # * The notation "[...](#name)" defines a link to an anchor, which is either
14
+ # the chunk with the specified name, or an empty anchor defined as above.
15
+ def self.to_html(markdown)
16
+ markdown = Markdown.embed_chunks(markdown)
17
+ markdown = Markdown.id_anchors(markdown)
18
+ html = RDiscount.new(markdown).to_html
19
+ html = Markdown.id_links(html)
20
+ return html.clean_markup_html
21
+ end
22
+
23
+ protected
24
+
25
+ # Expand "[[chunk|template]]" to HTML embed tags. Use identifiers instead of
26
+ # names in the +src+ field for safety, unless the template is a magical file
27
+ # template, in which case we must preserve the file path,
28
+ def self.embed_chunks(markdown)
29
+ return markdown.gsub(/\[\[(.*?)\|(.*?)\]\]/) do
30
+ src = $1
31
+ template = $2
32
+ src = src.to_id unless Codnar::Weaver::FILE_TEMPLATE_PROCESSORS.include?(template)
33
+ "<embed src='#{src}' type='x-codnar/#{template}'/>"
34
+ end
35
+ end
36
+
37
+ # Expand "[[#name]]" anchors to HTML anchor tags with the matching identifier.
38
+ def self.id_anchors(markdown)
39
+ return markdown.gsub(/\[\[#(.*?)\]\]/) { "<a id='#{$1.to_id}'/>" }
40
+ end
41
+
42
+ # Expand "href='#name'" links to the matching "href='#id'" links.
43
+ def self.id_links(html)
44
+ return html.gsub(/href=(["'])#(.*?)(["'])/) { "href=#{$1}##{$2.to_id}#{$3}" }
45
+ end
46
+
47
+ end
@@ -0,0 +1,138 @@
1
+ module Codnar
2
+
3
+ # Merge classified lines into chunks.
4
+ class Merger
5
+
6
+ # Convert classified lines from a disk file into chunks.
7
+ def self.chunks(errors, path, lines)
8
+ return Merger.new(errors, path, lines).chunks
9
+ end
10
+
11
+ # Return merged chunks containing the classified lines. Each chunk lines
12
+ # are only indented relative to the chunk. This allows nested chunks to be
13
+ # presented unindented in the final weaved HTML.
14
+ def chunks
15
+ @chunks = [ file_chunk ]
16
+ @stack = @chunks.dup
17
+ @errors.in_path(@path) { merge_lines }
18
+ @chunks.each { |chunk| Merger.unindent_lines(chunk.lines) }
19
+ return @chunks
20
+ end
21
+
22
+ protected
23
+
24
+ # Convert classified lines from a disk file into chunks.
25
+ def initialize(errors, path, lines)
26
+ @errors = errors
27
+ @path = path
28
+ @lines = lines
29
+ end
30
+
31
+ # The top-level all-the-disk-file chunk (without any classified lines)
32
+ def file_chunk
33
+ return {
34
+ "name" => @path,
35
+ "locations" => [ { "file" => @path, "line" => 1 } ],
36
+ "containers" => [],
37
+ "contained" => [],
38
+ "lines" => []
39
+ }
40
+ end
41
+
42
+ # {{{ Merging nested chunk lines
43
+
44
+ # Merge all the classified lines into chunks
45
+ def merge_lines
46
+ @lines.each do |line|
47
+ @errors.at_line(line.number)
48
+ merge_line(line)
49
+ end
50
+ end_unterminated_chunks
51
+ end
52
+
53
+ # End all chunks missing a terminating end chunk classified line.
54
+ def end_unterminated_chunks
55
+ @stack.shift
56
+ @stack.each do |chunk|
57
+ @errors << "Missing end line for chunk: #{chunk.name}"
58
+ end
59
+ end
60
+
61
+ # Merge the next classified line.
62
+ def merge_line(line)
63
+ case line.kind
64
+ when "begin_chunk"
65
+ begin_chunk_line(line)
66
+ when "end_chunk"
67
+ end_chunk_line(line)
68
+ else
69
+ @stack.last.lines << line
70
+ end
71
+ end
72
+
73
+ # Merge a classified line that starts a new chunk.
74
+ def begin_chunk_line(line)
75
+ chunk = contained_chunk(container = @stack.last, line)
76
+ container.contained << chunk.name
77
+ container.lines << line.merge("kind" => "nested_chunk")
78
+ @chunks << chunk
79
+ @stack << chunk
80
+ end
81
+
82
+ # A chunk contained in another chunk.
83
+ def contained_chunk(container, line)
84
+ return {
85
+ "name" => new_chunk_name(line.payload),
86
+ "locations" => [ { "file" => @path, "line" => line.number } ],
87
+ "containers" => [ container.name ],
88
+ "contained" => [],
89
+ "lines" => [ line ]
90
+ }
91
+ end
92
+
93
+ # Return the name of a new chunk.
94
+ def new_chunk_name(name)
95
+ return name unless name.nil? || name == ""
96
+ @errors << "Begin line for chunk with no name"
97
+ return "#{@path}/#{@chunks.size}"
98
+ end
99
+
100
+ # Merge a classified line that ends an existing chunk.
101
+ def end_chunk_line(line)
102
+ return missing_begin_chunk_line(line) if @stack.size == 1
103
+ chunk = @stack.last
104
+ @errors << "End line for chunk: #{line.payload} mismatches begin line for chunk: #{chunk.name}" \
105
+ unless Merger.matching_end_chunk_line?(chunk, line)
106
+ chunk.lines << line
107
+ @stack.pop
108
+ end
109
+
110
+ # Check whether an end chunk classified line matches the begin chunk
111
+ # classified line.
112
+ def self.matching_end_chunk_line?(chunk, line)
113
+ line_name = line.payload
114
+ return line_name.to_s == "" || line_name.to_id == chunk.name.to_id
115
+ end
116
+
117
+ # }}}
118
+
119
+ # {{{ Unindenting chunk lines
120
+
121
+ # Remove the common indentation from a sequence of classified lines.
122
+ def self.unindent_lines(lines)
123
+ indentation = Merger.minimal_indentation(lines)
124
+ lines.each do |line|
125
+ line.indentation = line.indentation.andand.unindent(indentation)
126
+ end
127
+ end
128
+
129
+ # Find out the minimal indentation of all the classified lines.
130
+ def self.minimal_indentation(lines)
131
+ return lines.map { |line| line.indentation }.compact.min
132
+ end
133
+
134
+ # }}}
135
+
136
+ end
137
+
138
+ end