markdown_helper 1.8.0 → 1.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +5 -5
  3. data/CODE_OF_CONDUCT.md +74 -74
  4. data/Gemfile.lock +2 -1
  5. data/LICENSE.txt +21 -21
  6. data/README.md +60 -47
  7. data/Rakefile +16 -13
  8. data/bin/_include +0 -1
  9. data/bin/_resolve +0 -1
  10. data/bin/markdown_helper +3 -0
  11. data/bin/setup +8 -8
  12. data/lib/markdown_helper.rb +329 -97
  13. data/lib/markdown_helper/version.rb +1 -1
  14. data/markdown/{README.template.md → readme/README.template.md} +25 -12
  15. data/markdown/{code_block_ruby_template.md → readme/code_block_ruby_template.md} +0 -0
  16. data/markdown/{highlight_ruby_template.md → readme/highlight_ruby_template.md} +0 -0
  17. data/markdown/readme/highlighted_ruby.md +12 -0
  18. data/markdown/{include.md → readme/include.md} +0 -0
  19. data/markdown/{include.rb → readme/include.rb} +0 -0
  20. data/markdown/{include_usage.rb → readme/include_usage.rb} +0 -0
  21. data/markdown/{resolve.md → readme/resolve.md} +0 -0
  22. data/markdown/{resolve_usage.rb → readme/resolve_usage.rb} +0 -0
  23. data/markdown/{verbatim_ruby_template.md → readme/verbatim_ruby_template.md} +0 -0
  24. data/markdown/use_cases/Rakefile +53 -23
  25. data/markdown/use_cases/include/diagnose_circular_includes/diagnose_circular_includes.err +26 -0
  26. data/markdown/use_cases/include/diagnose_circular_includes/diagnose_circular_includes.rb +65 -0
  27. data/markdown/use_cases/include/diagnose_circular_includes/include.rb +5 -0
  28. data/markdown/use_cases/include/diagnose_circular_includes/includer.md +1 -0
  29. data/markdown/use_cases/include/diagnose_circular_includes/includer_0.md +1 -0
  30. data/markdown/use_cases/include/diagnose_circular_includes/includer_1.md +1 -0
  31. data/markdown/use_cases/include/diagnose_circular_includes/includer_2.md +1 -0
  32. data/markdown/use_cases/include/diagnose_circular_includes/use_case.md +98 -0
  33. data/markdown/use_cases/include/diagnose_circular_includes/use_case_template.md +27 -0
  34. data/markdown/use_cases/include/diagnose_missing_includee/diagnose_missing_includee.err +26 -0
  35. data/markdown/use_cases/include/diagnose_missing_includee/diagnose_missing_includee.rb +67 -0
  36. data/markdown/use_cases/include/diagnose_missing_includee/include.rb +5 -0
  37. data/markdown/use_cases/include/diagnose_missing_includee/included.md +1 -0
  38. data/markdown/use_cases/include/diagnose_missing_includee/includer.md +1 -0
  39. data/markdown/use_cases/include/diagnose_missing_includee/includer_0.md +1 -0
  40. data/markdown/use_cases/include/diagnose_missing_includee/includer_1.md +1 -0
  41. data/markdown/use_cases/include/diagnose_missing_includee/includer_2.md +1 -0
  42. data/markdown/use_cases/include/diagnose_missing_includee/use_case.md +100 -0
  43. data/markdown/use_cases/include/diagnose_missing_includee/use_case_template.md +29 -0
  44. data/markdown/use_cases/include/include.rb +5 -0
  45. data/markdown/use_cases/include/include_code_block/hello.rb +8 -0
  46. data/markdown/use_cases/include/include_code_block/include_code_block.rb +79 -0
  47. data/markdown/use_cases/include/include_code_block/included.md +14 -0
  48. data/markdown/use_cases/include/include_code_block/includer.md +4 -0
  49. data/markdown/use_cases/include/include_code_block/use_case.md +108 -0
  50. data/markdown/use_cases/include/include_code_block/use_case_template.md +33 -0
  51. data/markdown/use_cases/include/include_generated_text/include_generated_text.rb +38 -0
  52. data/markdown/use_cases/include/include_generated_text/use_case.md +18 -0
  53. data/markdown/use_cases/include/include_generated_text/use_case_template.md +18 -0
  54. data/markdown/use_cases/include/include_highlighted_code/hello.rb +8 -0
  55. data/markdown/use_cases/include/include_highlighted_code/include_highlighted_code.rb +83 -0
  56. data/markdown/use_cases/include/include_highlighted_code/included.md +14 -0
  57. data/markdown/use_cases/include/include_highlighted_code/includer.md +4 -0
  58. data/markdown/use_cases/include/include_highlighted_code/use_case.md +110 -0
  59. data/markdown/use_cases/include/include_highlighted_code/use_case_template.md +35 -0
  60. data/markdown/use_cases/include/include_markdown/include_markdown.rb +81 -0
  61. data/markdown/use_cases/include/include_markdown/included.md +13 -0
  62. data/markdown/use_cases/include/include_markdown/includer.md +4 -0
  63. data/markdown/use_cases/include/include_markdown/markdown.md +10 -0
  64. data/markdown/use_cases/include/include_markdown/use_case.md +106 -0
  65. data/markdown/use_cases/include/include_markdown/use_case_template.md +33 -0
  66. data/markdown/use_cases/include/include_use_case.rb +110 -0
  67. data/markdown/use_cases/include/include_with_added_comments/include_with_added_comments.rb +43 -60
  68. data/markdown/use_cases/include/include_with_added_comments/included.md +2 -2
  69. data/markdown/use_cases/include/include_with_added_comments/use_case.md +56 -0
  70. data/markdown/use_cases/include/include_with_added_comments/{template.md → use_case_template.md} +20 -1
  71. data/markdown/use_cases/include/interface.md +25 -0
  72. data/markdown/use_cases/include/nest_inclusions/included.md +5 -0
  73. data/markdown/use_cases/include/nest_inclusions/includee.md +3 -0
  74. data/markdown/use_cases/include/nest_inclusions/includer.md +3 -0
  75. data/markdown/use_cases/include/nest_inclusions/nest_inclusions.rb +61 -0
  76. data/markdown/use_cases/include/nest_inclusions/nested_includee.md +1 -0
  77. data/markdown/use_cases/include/nest_inclusions/use_case.md +74 -0
  78. data/markdown/use_cases/include/nest_inclusions/use_case_template.md +23 -0
  79. data/markdown/use_cases/include/reuse_text/include.rb +5 -0
  80. data/markdown/use_cases/include/reuse_text/included.md +2 -5
  81. data/markdown/use_cases/include/reuse_text/includee.md +1 -0
  82. data/markdown/use_cases/include/reuse_text/includer.md +2 -5
  83. data/markdown/use_cases/include/reuse_text/reuse_text.rb +24 -77
  84. data/markdown/use_cases/include/reuse_text/use_case.md +69 -0
  85. data/markdown/use_cases/include/reuse_text/use_case_template.md +23 -0
  86. data/markdown/use_cases/resolve/gemify_images/gemify_images.rb +58 -29
  87. data/markdown/use_cases/resolve/gemify_images/template.md +4 -0
  88. data/markdown/use_cases/use_case.rb +45 -0
  89. data/markdown/use_cases/use_cases.md +9 -2
  90. data/markdown_helper.gemspec +1 -1
  91. metadata +70 -19
  92. data/markdown/highlighted_ruby.md +0 -12
  93. data/markdown/use_cases/include/include_generated_text/build.rb +0 -46
  94. data/markdown/use_cases/include/include_with_added_comments/include_with_added_comments.md +0 -37
  95. data/markdown/use_cases/include/reuse_text/reusable_text.md +0 -1
  96. data/markdown/use_cases/include/reuse_text/reuse_text.md +0 -54
  97. data/markdown/use_cases/include/reuse_text/template.md +0 -33
data/Rakefile CHANGED
@@ -1,6 +1,8 @@
1
1
  require 'bundler/gem_tasks'
2
2
  require 'rake/testtask'
3
3
 
4
+ require_relative 'lib/markdown_helper'
5
+
4
6
  Rake::TestTask.new(:test) do |t|
5
7
  t.libs << 'test'
6
8
  t.libs << 'lib'
@@ -12,19 +14,20 @@ namespace :build do
12
14
  desc 'Build README.md file from README.template.md'
13
15
  task :readme do
14
16
  Rake::Task['build:usages'].invoke
15
- require_relative 'lib/markdown_helper'
16
- markdown_helper = MarkdownHelper.new
17
- template_file_path = 'markdown/highlight_ruby_template.md'
18
- markdown_file_path = 'markdown/highlighted_ruby.md'
19
- markdown_helper.include(template_file_path, markdown_file_path)
20
- # Do the resolve before the include, so that the included text is not also resolved.
21
- # This protects example code from being also resolved, thus damaging the example code.
22
- # Temp file must be in the same directory as its source (it becomes the source).
23
- temp_file_path = 'markdown/temp_resolved.md'
24
- markdown_helper.resolve('markdown/README.template.md', temp_file_path)
25
- readme_file_path = 'README.md'
26
- markdown_helper.include(temp_file_path, readme_file_path)
27
- File.delete(temp_file_path)
17
+ Dir.chdir('markdown/readme') do
18
+ markdown_helper = MarkdownHelper.new
19
+ template_file_path = 'highlight_ruby_template.md'
20
+ markdown_file_path = 'highlighted_ruby.md'
21
+ markdown_helper.include(template_file_path, markdown_file_path)
22
+ # Do the resolve before the include, so that the included text is not also resolved.
23
+ # This protects example code from being also resolved, thus damaging the example code.
24
+ # Temp file must be in the same directory as its source (it becomes the source).
25
+ temp_file_path = 'temp_resolved.md'
26
+ markdown_helper.resolve('README.template.md', temp_file_path)
27
+ readme_file_path = '../../README.md'
28
+ markdown_helper.include(temp_file_path, readme_file_path)
29
+ File.delete(temp_file_path)
30
+ end
28
31
  end
29
32
 
30
33
  desc 'Build usage for executables'
@@ -42,6 +42,5 @@ parser.parse!
42
42
  _, template_file_path, markdown_file_path = ARGV
43
43
 
44
44
  usage(opts) unless ARGV.size == 3
45
- usage(opts) unless File.readable?(template_file_path)
46
45
 
47
46
  MarkdownHelper.new(options).include(template_file_path, markdown_file_path)
@@ -42,6 +42,5 @@ parser.parse!
42
42
  _, template_file_path, markdown_file_path = ARGV
43
43
 
44
44
  usage(opts) unless ARGV.size == 3
45
- usage(opts) unless File.readable?(template_file_path)
46
45
 
47
46
  MarkdownHelper.new(options).resolve(template_file_path, markdown_file_path)
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'markdown_helper'
4
4
 
5
+ # Confirm that we're in a git project.
6
+ MarkdownHelper.git_clone_dir_path
7
+
5
8
  # Each command foo has a corresponding Ruby executable _foo.
6
9
  def command_keywords
7
10
  dir_path = File.dirname(__FILE__)
data/bin/setup CHANGED
@@ -1,8 +1,8 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -7,19 +7,27 @@ require 'markdown_helper/version'
7
7
  # @author Burdette Lamar
8
8
  class MarkdownHelper
9
9
 
10
+ class MarkdownHelperError < RuntimeError; end
11
+ class CircularIncludeError < MarkdownHelperError; end
12
+ class MissingIncludeeError < MarkdownHelperError; end
13
+ class OptionError < MarkdownHelperError; end
14
+ class EnvironmentError < MarkdownHelperError; end
15
+
10
16
  IMAGE_REGEXP = /!\[([^\[]+)\]\(([^)]+)\)/
11
17
  INCLUDE_REGEXP = /^@\[([^\[]+)\]\(([^)]+)\)$/
12
18
 
13
19
  attr_accessor :pristine
14
20
 
15
21
  def initialize(options = {})
22
+ # Confirm that we're in a git project.
23
+ MarkdownHelper.git_clone_dir_path
16
24
  default_options = {
17
25
  :pristine => false,
18
26
  }
19
27
  merged_options = default_options.merge(options)
20
28
  merged_options.each_pair do |method, value|
21
29
  unless self.respond_to?(method)
22
- raise ArgumentError.new("Unknown option: #{method}")
30
+ raise OptionError.new("Unknown option: #{method}")
23
31
  end
24
32
  setter_method = "#{method}="
25
33
  send(setter_method, value)
@@ -42,7 +50,7 @@ class MarkdownHelper
42
50
  # @[:markdown](foo.md)
43
51
  def include(template_file_path, markdown_file_path)
44
52
  send(:generate_file, template_file_path, markdown_file_path, __method__) do |input_lines, output_lines|
45
- send(:include_files, template_file_path, input_lines, output_lines, markdown_inclusions = {})
53
+ send(:include_files, template_file_path, input_lines, output_lines, Inclusions.new)
46
54
  end
47
55
  end
48
56
 
@@ -77,7 +85,7 @@ class MarkdownHelper
77
85
 
78
86
  private
79
87
 
80
- def comment(text)
88
+ def self.comment(text)
81
89
  "<!--#{text}-->\n"
82
90
  end
83
91
 
@@ -86,27 +94,48 @@ class MarkdownHelper
86
94
  repo_name = ENV['REPO_NAME']
87
95
  unless repo_user and repo_name
88
96
  message = 'ENV values for both REPO_USER and REPO_NAME must be defined.'
89
- raise RuntimeError.new(message)
97
+ raise EnvironmentError.new(message)
90
98
  end
91
99
  [repo_user, repo_name]
92
100
  end
93
101
 
94
102
  def generate_file(template_file_path, markdown_file_path, method)
95
103
  output_lines = []
96
- File.open(template_file_path, 'r') do |template_file|
97
- output_lines.push(comment(" >>>>>> BEGIN GENERATED FILE (#{method.to_s}): SOURCE #{template_file_path} ")) unless pristine
98
- input_lines = template_file.readlines
99
- yield input_lines, output_lines
100
- output_lines.push(comment(" <<<<<< END GENERATED FILE (#{method.to_s}): SOURCE #{template_file_path} ")) unless pristine
104
+ begin
105
+ File.open(template_file_path, 'r') do |template_file|
106
+ template_path_in_project = MarkdownHelper.path_in_project(template_file_path)
107
+ output_lines.push(MarkdownHelper.comment(" >>>>>> BEGIN GENERATED FILE (#{method.to_s}): SOURCE #{template_path_in_project} ")) unless pristine
108
+ input_lines = template_file.readlines
109
+ yield input_lines, output_lines
110
+ output_lines.push(MarkdownHelper.comment(" <<<<<< END GENERATED FILE (#{method.to_s}): SOURCE #{template_path_in_project} ")) unless pristine
111
+ end
112
+ output = output_lines.join('')
113
+ rescue => e
114
+ unless e.kind_of?(MarkdownHelperError)
115
+ message = [
116
+ e.message,
117
+ Inclusions::UNREADABLE_TEMPLATE_EXCEPTION_LABEL,
118
+ ].join("\n")
119
+ e = e.exception(message)
120
+ end
121
+ raise e
101
122
  end
102
- output = output_lines.join('')
103
- File.open(markdown_file_path, 'w') do |md_file|
104
- md_file.write(output)
123
+ begin
124
+ File.write(markdown_file_path, output)
125
+ rescue => e
126
+ unless e.kind_of?(MarkdownHelperError)
127
+ message = [
128
+ e.message,
129
+ Inclusions::UNWRITABLE_OUTPUT_EXCEPTION_LABEL,
130
+ ].join("\n")
131
+ e = e.exception(message)
132
+ end
133
+ raise e
105
134
  end
106
135
  output
107
136
  end
108
137
 
109
- def include_files(includer_file_path, input_lines, output_lines, markdown_inclusions)
138
+ def include_files(includer_file_path, input_lines, output_lines, inclusions)
110
139
 
111
140
  input_lines.each_with_index do |input_line, line_index|
112
141
  match_data = input_line.match(INCLUDE_REGEXP)
@@ -115,73 +144,42 @@ class MarkdownHelper
115
144
  next
116
145
  end
117
146
  treatment = match_data[1]
118
- relative_included_file_path = match_data[2]
119
- treatment = case treatment
120
- when ':code_block'
121
- :code_block
122
- when ':markdown'
123
- :markdown
124
- when ':verbatim'
125
- message = "Treatment ':verbatim' is deprecated; please use treatment ':markdown'."
126
- warn(message)
127
- :markdown
128
- when ':comment'
129
- :comment
130
- else
131
- treatment
132
- end
133
- new_inclusion = Inclusion.new(
147
+ cited_includee_file_path = match_data[2]
148
+ inclusions.include(
149
+ input_line.chomp,
134
150
  includer_file_path,
135
- includer_line_number = line_index + 1,
136
- relative_included_file_path
151
+ line_index + 1,
152
+ cited_includee_file_path,
153
+ treatment,
154
+ output_lines,
155
+ self
137
156
  )
138
- included_real_path = new_inclusion.included_real_path
139
- if treatment == :markdown
140
- previously_included = markdown_inclusions.include?(included_real_path)
141
- if previously_included
142
- markdown_inclusions.store(included_real_path, new_inclusion)
143
- message_lines = ['Includes are circular:']
144
- i = 0
145
- markdown_inclusions.each_with_index do |path_and_inclusion, i|
146
- _, inclusion = *path_and_inclusion
147
- message_lines.push(" Level #{i}:")
148
- message_lines.push(" Includer: #{inclusion.includer_file_path}:#{inclusion.includer_line_number}")
149
- message_lines.push(" Relative file path: #{inclusion.relative_included_file_path}")
150
- message_lines.push(" Included file path: #{inclusion.included_file_path}")
151
- message_lines.push(" Real file_path: #{inclusion.included_real_path}")
152
- end
153
- message = message_lines.join("\n")
154
- raise RuntimeError.new(message)
155
- end
156
- end
157
- output_lines.push(comment(" >>>>>> BEGIN INCLUDED FILE (#{treatment}): SOURCE #{new_inclusion.included_file_path} ")) unless pristine
158
- include_lines = File.readlines(new_inclusion.included_file_path)
159
- unless include_lines.last.match("\n")
160
- message = "Warning: Included file has no trailing newline: #{relative_included_file_path}"
161
- warn(message)
162
- end
163
- case treatment
164
- when :markdown
165
- # Pass through unadorned, but honor any nested includes.
166
- markdown_inclusions.store(included_real_path, new_inclusion)
167
- include_files(new_inclusion.included_file_path, include_lines, output_lines, markdown_inclusions)
168
- markdown_inclusions.delete(included_real_path)
169
- when :comment
170
- output_lines.push(comment(include_lines.join('')))
171
- else
172
- # Use the file name as a label.
173
- file_name_line = format("<code>%s</code>\n", File.basename(relative_included_file_path))
174
- output_lines.push(file_name_line)
175
- # Put into code block.
176
- language = treatment == :code_block ? '' : treatment
177
- output_lines.push("```#{language}\n")
178
- output_lines.push(*include_lines)
179
- output_lines.push("```\n")
180
- end
181
- output_lines.push(comment(" <<<<<< END INCLUDED FILE (#{treatment}): SOURCE #{new_inclusion.included_file_path} ")) unless pristine
182
157
  end
183
158
  end
184
159
 
160
+ def self.git_clone_dir_path
161
+ git_dir = `git rev-parse --git-dir`.chomp
162
+ unless $?.success?
163
+ message = <<EOT
164
+
165
+ Markdown helper must run inside a .git project.
166
+ That is, the working directory one of its parents must be a .git directory.
167
+ EOT
168
+ raise RuntimeError.new(message)
169
+ end
170
+ if git_dir == '.git'
171
+ path = `pwd`.chomp
172
+ else
173
+ path = File.dirname(git_dir).chomp
174
+ end
175
+ realpath = Pathname.new(path.sub(%r|/c/|, 'C:/')).realpath
176
+ realpath.to_s
177
+ end
178
+
179
+ def self.path_in_project(path)
180
+ path.sub(MarkdownHelper.git_clone_dir_path + '/', '')
181
+ end
182
+
185
183
  def resolve_images(template_file_path, input_lines, output_lines)
186
184
  input_lines.each do |input_line|
187
185
  scan_data = input_line.scan(IMAGE_REGEXP)
@@ -189,7 +187,7 @@ class MarkdownHelper
189
187
  output_lines.push(input_line)
190
188
  next
191
189
  end
192
- output_lines.push(comment(" >>>>>> BEGIN RESOLVED IMAGES: INPUT-LINE '#{input_line}' ")) unless pristine
190
+ output_lines.push(MarkdownHelper.comment(" >>>>>> BEGIN RESOLVED IMAGES: INPUT-LINE '#{input_line}' ")) unless pristine
193
191
  output_line = input_line
194
192
  scan_data.each do |alt_text, path_and_attributes|
195
193
  original_image_file_path, attributes_s = path_and_attributes.split(/\s?\|\s?/, 2)
@@ -206,14 +204,6 @@ class MarkdownHelper
206
204
  if original_image_file_path.start_with?('http')
207
205
  image_path = original_image_file_path
208
206
  else
209
- git_dir = `git rev-parse --git-dir`.chomp
210
- if git_dir == '.git'
211
- git_clone_dir_path = `pwd`.chomp
212
- else
213
- git_clone_dir_path = git_dir.chomp
214
- end
215
- git_clone_dir_pathname = Pathname.new(git_clone_dir_path.sub(%r|/c/|, 'C:/')).realpath
216
- git_clone_dir_path = git_clone_dir_pathname.to_s
217
207
  absolute_template_file_path = File.absolute_path(template_file_path)
218
208
  template_dir_path = File.dirname(absolute_template_file_path)
219
209
  absolute_file_path = File.join(
@@ -221,7 +211,7 @@ class MarkdownHelper
221
211
  original_image_file_path,
222
212
  )
223
213
  absolute_file_path = Pathname.new(absolute_file_path).cleanpath.to_s
224
- relative_image_file_path = absolute_file_path.sub(git_clone_dir_path + '/', '')
214
+ relative_image_file_path = MarkdownHelper.path_in_project(absolute_file_path)
225
215
  repo_user, repo_name = repo_user_and_name
226
216
  image_path = File.join(
227
217
  "https://raw.githubusercontent.com/#{repo_user}/#{repo_name}/master",
@@ -237,34 +227,276 @@ class MarkdownHelper
237
227
  output_line = output_line.sub(IMAGE_REGEXP, img_element)
238
228
  end
239
229
  output_lines.push(output_line)
240
- output_lines.push(comment(" <<<<<< END RESOLVED IMAGES: INPUT-LINE '#{input_line}' ")) unless pristine
230
+ output_lines.push(MarkdownHelper.comment(" <<<<<< END RESOLVED IMAGES: INPUT-LINE '#{input_line}' ")) unless pristine
231
+ end
232
+
233
+ end
234
+
235
+ class Inclusions
236
+
237
+ attr_accessor :inclusions
238
+
239
+ def initialize
240
+ self.inclusions = []
241
+ end
242
+
243
+ def include(
244
+ include_description,
245
+ includer_file_path,
246
+ includer_line_number,
247
+ cited_includee_file_path,
248
+ treatment,
249
+ output_lines,
250
+ markdown_helper
251
+ )
252
+ treatment = case treatment
253
+ when ':code_block'
254
+ :code_block
255
+ when ':markdown'
256
+ :markdown
257
+ when ':verbatim'
258
+ message = "Treatment ':verbatim' is deprecated; please use treatment ':markdown'."
259
+ warn(message)
260
+ :markdown
261
+ when ':comment'
262
+ :comment
263
+ when ':pre'
264
+ :pre
265
+ else
266
+ treatment
267
+ end
268
+ new_inclusion = Inclusion.new(
269
+ include_description,
270
+ includer_file_path,
271
+ includer_line_number,
272
+ cited_includee_file_path
273
+ )
274
+ if treatment == :markdown
275
+ check_circularity(new_inclusion)
276
+ end
277
+ includee_path_in_project = MarkdownHelper.path_in_project(new_inclusion.absolute_includee_file_path)
278
+ output_lines.push(MarkdownHelper.comment(" >>>>>> BEGIN INCLUDED FILE (#{treatment}): SOURCE #{includee_path_in_project} ")) unless markdown_helper.pristine
279
+ begin
280
+ include_lines = File.readlines(new_inclusion.absolute_includee_file_path)
281
+ rescue => e
282
+ inclusions.push(new_inclusion)
283
+ message = [
284
+ MISSING_INCLUDEE_EXCEPTION_LABEL,
285
+ backtrace_inclusions,
286
+ ].join("\n")
287
+ e = MissingIncludeeError.new(message)
288
+ e.set_backtrace([])
289
+ raise e
290
+ end
291
+ unless include_lines.last.match("\n")
292
+ message = "Warning: Included file has no trailing newline: #{cited_includee_file_path}"
293
+ warn(message)
294
+ end
295
+ case treatment
296
+ when :markdown
297
+ # Pass through unadorned, but honor any nested includes.
298
+ inclusions.push(new_inclusion)
299
+ markdown_helper.send(:include_files, new_inclusion.absolute_includee_file_path, include_lines, output_lines, self)
300
+ inclusions.pop
301
+ when :comment
302
+ output_lines.push(MarkdownHelper.comment(include_lines.join('')))
303
+ when :pre
304
+ output_lines.push("<pre>\n")
305
+ output_lines.push(include_lines.join(''))
306
+ output_lines.push("</pre>\n")
307
+ else
308
+ # Use the file name as a label.
309
+ file_name_line = format("```%s```:\n", File.basename(cited_includee_file_path))
310
+ output_lines.push(file_name_line)
311
+ # Put into code block.
312
+ language = treatment == :code_block ? '' : treatment
313
+ output_lines.push("```#{language}\n")
314
+ output_lines.push(*include_lines)
315
+ output_lines.push("```\n")
316
+ end
317
+ output_lines.push(MarkdownHelper.comment(" <<<<<< END INCLUDED FILE (#{treatment}): SOURCE #{includee_path_in_project} ")) unless markdown_helper.pristine
318
+ end
319
+
320
+ CIRCULAR_EXCEPTION_LABEL = 'Includes are circular:'
321
+ UNREADABLE_TEMPLATE_EXCEPTION_LABEL = 'Could not read template file.'
322
+ UNWRITABLE_OUTPUT_EXCEPTION_LABEL = 'Could not write markdown file.'
323
+ MISSING_INCLUDEE_EXCEPTION_LABEL = 'Could not read include file,'
324
+ LEVEL_LABEL = ' Level'
325
+ BACKTRACE_LABEL = ' Backtrace (innermost include first):'
326
+
327
+ def check_circularity(new_inclusion)
328
+ previous_inclusions = inclusions.collect {|x| x.real_includee_file_path}
329
+ previously_included = previous_inclusions.include?(new_inclusion.real_includee_file_path)
330
+ if previously_included
331
+ inclusions.push(new_inclusion)
332
+ message = [
333
+ CIRCULAR_EXCEPTION_LABEL,
334
+ backtrace_inclusions,
335
+ ].join("\n")
336
+ e = MarkdownHelper::CircularIncludeError.new(message)
337
+ e.set_backtrace([])
338
+ raise e
339
+ end
340
+ end
341
+
342
+ def backtrace_inclusions
343
+ lines = [BACKTRACE_LABEL]
344
+ inclusions.reverse.each_with_index do |inclusion, i|
345
+ lines.push("#{LEVEL_LABEL} #{i}:")
346
+ level_lines = inclusion.to_lines(indentation_level = 3)
347
+ lines.push(*level_lines)
348
+ end
349
+ lines.join("\n")
350
+ end
351
+
352
+ def self.assert_io_exception(test, expected_exception_class, exception_label, e)
353
+ test.assert_kind_of(expected_exception_class, e)
354
+ lines = e.message.split("\n")
355
+ _ = lines.shift # Message from original exception.
356
+ label_line = lines.shift
357
+ test.assert_equal(exception_label, label_line)
358
+ end
359
+
360
+ def self.assert_inclusion_exception(test, expected_exception_class, exception_label, expected_inclusions, e)
361
+ test.assert_kind_of(expected_exception_class, e)
362
+ lines = e.message.split("\n")
363
+ label_line = lines.shift
364
+ test.assert_equal(exception_label, label_line)
365
+ backtrace_line = lines.shift
366
+ test.assert_equal(BACKTRACE_LABEL, backtrace_line)
367
+ level_line_count = 1 + Inclusion::LINE_COUNT
368
+ level_count = lines.size / level_line_count
369
+ # Backtrace levels are innermost first, opposite of inclusions.
370
+ reversed_inclusions = expected_inclusions.inclusions.reverse
371
+ (0...level_count).each do |level_index|
372
+ level_line = lines.shift
373
+ inclusion_lines = lines.shift(Inclusion::LINE_COUNT)
374
+ test.assert_equal("#{LEVEL_LABEL} #{level_index}:", level_line)
375
+ expected_inclusion = reversed_inclusions[level_index]
376
+ expected_inclusion.assert_lines(test, level_index, inclusion_lines)
377
+ end
378
+ end
379
+
380
+ def self.assert_circular_exception(test, expected_inclusions, e)
381
+ self.assert_inclusion_exception(
382
+ test,
383
+ CircularIncludeError,
384
+ CIRCULAR_EXCEPTION_LABEL,
385
+ expected_inclusions,
386
+ e
387
+ )
388
+ end
389
+
390
+ def self.assert_includee_missing_exception(test, expected_inclusions, e)
391
+ self.assert_inclusion_exception(
392
+ test,
393
+ Exception,
394
+ MISSING_INCLUDEE_EXCEPTION_LABEL,
395
+ expected_inclusions,
396
+ e
397
+ )
398
+ end
399
+
400
+ def self.assert_template_exception(test, e)
401
+ self.assert_io_exception(
402
+ test,
403
+ Exception,
404
+ UNREADABLE_TEMPLATE_EXCEPTION_LABEL,
405
+ e
406
+ )
407
+ end
408
+
409
+ def self.assert_output_exception(test, e)
410
+ self.assert_io_exception(
411
+ test,
412
+ Exception,
413
+ UNWRITABLE_OUTPUT_EXCEPTION_LABEL,
414
+ e
415
+ )
241
416
  end
242
417
 
243
418
  end
244
419
 
245
420
  class Inclusion
246
421
 
422
+ LINE_COUNT = 5
423
+
247
424
  attr_accessor \
248
425
  :includer_file_path,
249
426
  :includer_line_number,
250
- :relative_included_file_path,
251
- :included_file_path,
252
- :included_real_path
427
+ :include_description,
428
+ :absolute_includee_file_path,
429
+ :cited_includee_file_path,
430
+ :include_description
253
431
 
254
432
  def initialize(
433
+ include_description,
255
434
  includer_file_path,
256
435
  includer_line_number,
257
- relative_included_file_path
436
+ cited_includee_file_path
258
437
  )
259
- included_file_path = File.join(
260
- File.dirname(includer_file_path),
261
- relative_included_file_path,
262
- )
438
+ self.include_description = include_description
263
439
  self.includer_file_path = includer_file_path
264
440
  self.includer_line_number = includer_line_number
265
- self.relative_included_file_path = relative_included_file_path
266
- self.included_file_path = included_file_path
267
- self.included_real_path = Pathname.new(included_file_path).realpath.to_s
441
+ self.cited_includee_file_path = cited_includee_file_path
442
+ self.absolute_includee_file_path = absolute_includee_file_path
443
+ self.absolute_includee_file_path = File.absolute_path(File.join(
444
+ File.dirname(includer_file_path),
445
+ cited_includee_file_path,
446
+ ))
447
+ end
448
+
449
+ def real_includee_file_path
450
+ # Would raise exception unless exists.
451
+ return nil unless File.exist?(absolute_includee_file_path)
452
+ Pathname.new(absolute_includee_file_path).realpath.to_s
453
+ end
454
+
455
+ def to_lines(indentation_level)
456
+ def indentation(level)
457
+ ' ' * level
458
+ end
459
+ relative_inluder_file_path = MarkdownHelper.path_in_project(includer_file_path)
460
+ relative_inludee_file_path = MarkdownHelper.path_in_project(absolute_includee_file_path)
461
+ text = <<EOT
462
+ #{indentation(indentation_level)}Includer:
463
+ #{indentation(indentation_level+1)}Location: #{relative_inluder_file_path}:#{includer_line_number}
464
+ #{indentation(indentation_level+1)}Include description: #{include_description}
465
+ #{indentation(indentation_level)}Includee:
466
+ #{indentation(indentation_level+1)}File path: #{relative_inludee_file_path}
467
+ EOT
468
+ text.split("\n")
469
+ end
470
+
471
+ def assert_lines(test, level_index, actual_lines)
472
+ level_label = "Level #{level_index}:"
473
+ # Includer label.
474
+ includee_label = actual_lines.shift
475
+ test.assert_match(/^\s*Includer:$/, includee_label, level_label)
476
+ # Includer locatioin.
477
+ location = actual_lines.shift
478
+ message = "#{level_label} includer location"
479
+ test.assert_match(/^\s*Location:/, location, message)
480
+ includer_realpath = Pathname.new(includer_file_path).realpath.to_s
481
+ relative_path = MarkdownHelper.path_in_project(includer_realpath)
482
+ r = Regexp.new(Regexp.escape("#{relative_path}:#{includer_line_number}") + '$')
483
+ test.assert_match(r, location, message)
484
+ # Include description.
485
+ description = actual_lines.shift
486
+ message = "#{level_label} include description"
487
+ test.assert_match(/^\s*Include description:/, description, message)
488
+ r = Regexp.new(Regexp.escape("#{include_description}") + '$')
489
+ test.assert_match(r, description, message)
490
+ # Includee label.
491
+ includee_label = actual_lines.shift
492
+ test.assert_match(/^\s*Includee:$/, includee_label, level_label)
493
+ # Includee file path.
494
+ includee_file_path = actual_lines.shift
495
+ message = "#{level_label} includee cited file path"
496
+ test.assert_match(/^\s*File path:/, includee_file_path, message)
497
+ relative_path = MarkdownHelper.path_in_project(absolute_includee_file_path)
498
+ r = Regexp.new(Regexp.escape("#{relative_path}") + '$')
499
+ test.assert_match(r, includee_file_path, message)
268
500
  end
269
501
 
270
502
  end