markdown_helper 1.8.0 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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