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.
- data/ChangeLog +165 -0
- data/LICENSE +19 -0
- data/README.rdoc +32 -0
- data/Rakefile +66 -0
- data/bin/codnar-split +5 -0
- data/bin/codnar-weave +5 -0
- data/codnar.html +10945 -0
- data/doc/logo.png +0 -0
- data/doc/root.html +22 -0
- data/doc/story.markdown +180 -0
- data/doc/system.markdown +671 -0
- data/lib/codnar.rb +41 -0
- data/lib/codnar/application.rb +92 -0
- data/lib/codnar/cache.rb +61 -0
- data/lib/codnar/data/contents.js +113 -0
- data/lib/codnar/data/control_chunks.js +44 -0
- data/lib/codnar/data/style.css +95 -0
- data/lib/codnar/data/sunlight/README.txt +4 -0
- data/lib/codnar/data/sunlight/css-min.js +1 -0
- data/lib/codnar/data/sunlight/default.css +236 -0
- data/lib/codnar/data/sunlight/javascript-min.js +1 -0
- data/lib/codnar/data/sunlight/min.js +1 -0
- data/lib/codnar/data/sunlight/ruby-min.js +1 -0
- data/lib/codnar/data/yui/README.txt +3 -0
- data/lib/codnar/data/yui/base.css +132 -0
- data/lib/codnar/data/yui/reset.css +142 -0
- data/lib/codnar/formatter.rb +180 -0
- data/lib/codnar/grouper.rb +28 -0
- data/lib/codnar/gvim.rb +132 -0
- data/lib/codnar/hash_extensions.rb +41 -0
- data/lib/codnar/markdown.rb +47 -0
- data/lib/codnar/merger.rb +138 -0
- data/lib/codnar/rake.rb +41 -0
- data/lib/codnar/rake/split_task.rb +71 -0
- data/lib/codnar/rake/weave_task.rb +59 -0
- data/lib/codnar/rdoc.rb +9 -0
- data/lib/codnar/reader.rb +121 -0
- data/lib/codnar/scanner.rb +216 -0
- data/lib/codnar/split.rb +58 -0
- data/lib/codnar/split_configurations.rb +367 -0
- data/lib/codnar/splitter.rb +32 -0
- data/lib/codnar/string_extensions.rb +25 -0
- data/lib/codnar/sunlight.rb +17 -0
- data/lib/codnar/version.rb +8 -0
- data/lib/codnar/weave.rb +58 -0
- data/lib/codnar/weave_configurations.rb +48 -0
- data/lib/codnar/weaver.rb +105 -0
- data/lib/codnar/writer.rb +38 -0
- data/test/cache_computations.rb +41 -0
- data/test/deep_merge.rb +29 -0
- data/test/embed_images.rb +12 -0
- data/test/expand_markdown.rb +27 -0
- data/test/expand_rdoc.rb +20 -0
- data/test/format_code_gvim_configurations.rb +55 -0
- data/test/format_code_sunlight_configurations.rb +37 -0
- data/test/format_comment_configurations.rb +86 -0
- data/test/format_lines.rb +72 -0
- data/test/group_lines.rb +31 -0
- data/test/gvim_highlight_syntax.rb +49 -0
- data/test/identify_chunks.rb +32 -0
- data/test/lib/test_with_configurations.rb +15 -0
- data/test/merge_lines.rb +133 -0
- data/test/rake_tasks.rb +38 -0
- data/test/read_chunks.rb +110 -0
- data/test/run_application.rb +56 -0
- data/test/run_split.rb +38 -0
- data/test/run_weave.rb +75 -0
- data/test/scan_lines.rb +78 -0
- data/test/split_chunk_configurations.rb +55 -0
- data/test/split_code.rb +109 -0
- data/test/split_code_configurations.rb +73 -0
- data/test/split_combined_configurations.rb +114 -0
- data/test/split_complex_comment_configurations.rb +73 -0
- data/test/split_documentation.rb +92 -0
- data/test/split_documentation_configurations.rb +97 -0
- data/test/split_simple_comment_configurations.rb +50 -0
- data/test/sunlight_highlight_syntax.rb +25 -0
- data/test/weave_configurations.rb +144 -0
- data/test/write_chunks.rb +28 -0
- 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
|
data/lib/codnar/gvim.rb
ADDED
@@ -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
|