docyard 0.5.0 → 0.6.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 (33) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +20 -1
  4. data/lib/docyard/build/static_generator.rb +1 -1
  5. data/lib/docyard/components/base_processor.rb +6 -0
  6. data/lib/docyard/components/code_block_diff_preprocessor.rb +104 -0
  7. data/lib/docyard/components/code_block_feature_extractor.rb +113 -0
  8. data/lib/docyard/components/code_block_focus_preprocessor.rb +77 -0
  9. data/lib/docyard/components/code_block_icon_detector.rb +40 -0
  10. data/lib/docyard/components/code_block_line_wrapper.rb +46 -0
  11. data/lib/docyard/components/code_block_options_preprocessor.rb +76 -0
  12. data/lib/docyard/components/code_block_patterns.rb +51 -0
  13. data/lib/docyard/components/code_block_processor.rb +135 -14
  14. data/lib/docyard/components/code_line_parser.rb +80 -0
  15. data/lib/docyard/components/code_snippet_import_preprocessor.rb +125 -0
  16. data/lib/docyard/components/registry.rb +4 -4
  17. data/lib/docyard/components/table_of_contents_processor.rb +1 -1
  18. data/lib/docyard/components/tabs_parser.rb +135 -4
  19. data/lib/docyard/components/tabs_range_finder.rb +42 -0
  20. data/lib/docyard/config/validator.rb +8 -0
  21. data/lib/docyard/config.rb +7 -0
  22. data/lib/docyard/icons/file_types.rb +0 -13
  23. data/lib/docyard/markdown.rb +13 -5
  24. data/lib/docyard/rack_application.rb +1 -1
  25. data/lib/docyard/renderer.rb +4 -3
  26. data/lib/docyard/templates/assets/css/code.css +12 -4
  27. data/lib/docyard/templates/assets/css/components/code-block.css +427 -24
  28. data/lib/docyard/templates/assets/css/components/navigation.css +12 -9
  29. data/lib/docyard/templates/assets/css/components/tabs.css +50 -44
  30. data/lib/docyard/templates/assets/css/variables.css +44 -0
  31. data/lib/docyard/templates/partials/_code_block.html.erb +50 -2
  32. data/lib/docyard/version.rb +1 -1
  33. metadata +11 -1
@@ -1,28 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "../icons"
4
+ require_relative "../language_mapping"
4
5
  require_relative "../renderer"
5
6
  require_relative "base_processor"
7
+ require_relative "code_block_icon_detector"
8
+ require_relative "code_block_line_wrapper"
9
+ require_relative "tabs_range_finder"
6
10
 
7
11
  module Docyard
8
12
  module Components
9
13
  class CodeBlockProcessor < BaseProcessor
14
+ include CodeBlockLineWrapper
15
+
10
16
  self.priority = 20
11
17
 
12
18
  def postprocess(html)
13
19
  return html unless html.include?('<div class="highlight">')
14
20
 
15
- html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
16
- process_code_block(Regexp.last_match(0), Regexp.last_match(1))
17
- end
21
+ initialize_postprocess_state(html)
22
+ process_all_highlight_blocks(html)
18
23
  end
19
24
 
20
25
  private
21
26
 
27
+ def initialize_postprocess_state(html)
28
+ @block_index = 0
29
+ @options = context[:code_block_options] || []
30
+ @diff_lines = context[:code_block_diff_lines] || []
31
+ @focus_lines = context[:code_block_focus_lines] || []
32
+ @error_lines = context[:code_block_error_lines] || []
33
+ @warning_lines = context[:code_block_warning_lines] || []
34
+ @global_line_numbers = context.dig(:config, "markdown", "lineNumbers") || false
35
+ @tabs_ranges = TabsRangeFinder.find_ranges(html)
36
+ end
37
+
38
+ def process_all_highlight_blocks(html)
39
+ result = +""
40
+ last_end = 0
41
+
42
+ html.scan(%r{<div class="highlight">(.*?)</div>}m) do
43
+ match = Regexp.last_match
44
+ result << html[last_end...match.begin(0)]
45
+ result << process_highlight_match(match)
46
+ last_end = match.end(0)
47
+ end
48
+
49
+ result << html[last_end..]
50
+ end
51
+
52
+ def process_highlight_match(match)
53
+ if inside_tabs?(match.begin(0))
54
+ match[0]
55
+ else
56
+ processed = process_code_block(match[0], match[1])
57
+ @block_index += 1
58
+ processed
59
+ end
60
+ end
61
+
22
62
  def process_code_block(original_html, inner_html)
63
+ block_data = extract_block_data(inner_html)
64
+ processed_html = process_html_for_highlighting(original_html, block_data)
65
+
66
+ render_code_block_with_copy(block_data.merge(html: processed_html))
67
+ end
68
+
69
+ def extract_block_data(inner_html)
70
+ opts = current_block_options
23
71
  code_text = extract_code_text(inner_html)
72
+ start_line = extract_start_line(opts[:option])
73
+ show_line_numbers = determine_line_numbers(opts[:option])
74
+ title_data = CodeBlockIconDetector.detect(opts[:title], opts[:lang])
24
75
 
25
- render_code_block_with_copy(original_html, code_text)
76
+ build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
77
+ end
78
+
79
+ def current_block_options
80
+ block_opts = @options[@block_index] || {}
81
+ {
82
+ option: block_opts[:option],
83
+ title: block_opts[:title],
84
+ lang: block_opts[:lang],
85
+ highlights: block_opts[:highlights] || []
86
+ }
87
+ end
88
+
89
+ def build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
90
+ {
91
+ text: code_text,
92
+ highlights: opts[:highlights],
93
+ diff_lines: @diff_lines[@block_index] || {},
94
+ focus_lines: @focus_lines[@block_index] || {},
95
+ error_lines: @error_lines[@block_index] || {},
96
+ warning_lines: @warning_lines[@block_index] || {},
97
+ show_line_numbers: show_line_numbers,
98
+ line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
99
+ start_line: start_line,
100
+ title: title_data[:title],
101
+ icon: title_data[:icon],
102
+ icon_source: title_data[:icon_source]
103
+ }
104
+ end
105
+
106
+ def process_html_for_highlighting(original_html, block_data)
107
+ needs_wrapping = block_data[:highlights].any? || block_data[:diff_lines].any? ||
108
+ block_data[:focus_lines].any? || block_data[:error_lines].any? ||
109
+ block_data[:warning_lines].any?
110
+ return original_html unless needs_wrapping
111
+
112
+ wrap_code_block(original_html, block_data)
113
+ end
114
+
115
+ def determine_line_numbers(block_option)
116
+ return false if block_option == ":no-line-numbers"
117
+ return true if block_option&.start_with?(":line-numbers")
118
+
119
+ @global_line_numbers
120
+ end
121
+
122
+ def extract_start_line(block_option)
123
+ return 1 unless block_option&.include?("=")
124
+
125
+ block_option.split("=").last.to_i
126
+ end
127
+
128
+ def generate_line_numbers(code_text, start_line)
129
+ line_count = code_text.lines.count
130
+ line_count = 1 if line_count.zero?
131
+ (start_line...(start_line + line_count)).to_a
132
+ end
133
+
134
+ def inside_tabs?(position)
135
+ @tabs_ranges.any? { |range| range.cover?(position) }
26
136
  end
27
137
 
28
138
  def extract_code_text(html)
@@ -31,17 +141,28 @@ module Docyard
31
141
  text.strip
32
142
  end
33
143
 
34
- def render_code_block_with_copy(code_block_html, code_text)
35
- copy_icon = Icons.render("copy", "regular") || ""
36
- renderer = Renderer.new
144
+ def render_code_block_with_copy(block_data)
145
+ Renderer.new.render_partial("_code_block", template_locals(block_data))
146
+ end
147
+
148
+ def template_locals(block_data)
149
+ base_locals(block_data).merge(line_feature_locals(block_data)).merge(title_locals(block_data))
150
+ end
151
+
152
+ def base_locals(block_data)
153
+ { code_block_html: block_data[:html], code_text: escape_html_attribute(block_data[:text]),
154
+ copy_icon: Icons.render("copy", "regular") || "", show_line_numbers: block_data[:show_line_numbers],
155
+ line_numbers: block_data[:line_numbers], start_line: block_data[:start_line] }
156
+ end
157
+
158
+ def line_feature_locals(block_data)
159
+ { highlights: block_data[:highlights], diff_lines: block_data[:diff_lines],
160
+ focus_lines: block_data[:focus_lines], error_lines: block_data[:error_lines],
161
+ warning_lines: block_data[:warning_lines] }
162
+ end
37
163
 
38
- renderer.render_partial(
39
- "_code_block", {
40
- code_block_html: code_block_html,
41
- code_text: escape_html_attribute(code_text),
42
- copy_icon: copy_icon
43
- }
44
- )
164
+ def title_locals(block_data)
165
+ { title: block_data[:title], icon: block_data[:icon], icon_source: block_data[:icon_source] }
45
166
  end
46
167
 
47
168
  def escape_html_attribute(text)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ class CodeLineParser
6
+ def initialize(code_content)
7
+ @code_content = code_content
8
+ @lines = []
9
+ @current_line = ""
10
+ @open_tags = []
11
+ @in_tag = false
12
+ @tag_buffer = ""
13
+ end
14
+
15
+ def parse
16
+ @code_content.each_char { |char| process_char(char) }
17
+ finalize
18
+ end
19
+
20
+ private
21
+
22
+ def process_char(char)
23
+ case char
24
+ when "<" then start_tag(char)
25
+ when ">" then end_tag_if_applicable(char)
26
+ when "\n" then handle_newline
27
+ else handle_regular_char(char)
28
+ end
29
+ end
30
+
31
+ def start_tag(char)
32
+ @in_tag = true
33
+ @tag_buffer = char
34
+ end
35
+
36
+ def end_tag_if_applicable(char)
37
+ if @in_tag
38
+ @in_tag = false
39
+ @tag_buffer += char
40
+
41
+ if @tag_buffer.start_with?("</")
42
+ @open_tags.pop
43
+ elsif !@tag_buffer.end_with?("/>")
44
+ @open_tags << @tag_buffer
45
+ end
46
+
47
+ @current_line += @tag_buffer
48
+ @tag_buffer = ""
49
+ else
50
+ @current_line += char
51
+ end
52
+ end
53
+
54
+ def handle_newline
55
+ closing_tags = @open_tags.reverse.map { |tag| closing_tag_for(tag) }.join
56
+ @lines << "#{@current_line}#{closing_tags}\n"
57
+ @current_line = @open_tags.join
58
+ end
59
+
60
+ def handle_regular_char(char)
61
+ if @in_tag
62
+ @tag_buffer += char
63
+ else
64
+ @current_line += char
65
+ end
66
+ end
67
+
68
+ def finalize
69
+ @lines << @current_line unless @current_line.empty?
70
+ @lines << "" if @lines.empty?
71
+ @lines
72
+ end
73
+
74
+ def closing_tag_for(open_tag)
75
+ tag_name = open_tag.match(/<(\w+)/)[1]
76
+ "</#{tag_name}>"
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_processor"
4
+
5
+ module Docyard
6
+ module Components
7
+ class CodeSnippetImportPreprocessor < BaseProcessor
8
+ EXTENSION_MAP = {
9
+ "rb" => "ruby",
10
+ "js" => "javascript",
11
+ "ts" => "typescript",
12
+ "py" => "python",
13
+ "yml" => "yaml",
14
+ "md" => "markdown",
15
+ "sh" => "bash",
16
+ "zsh" => "bash",
17
+ "jsx" => "jsx",
18
+ "tsx" => "tsx"
19
+ }.freeze
20
+
21
+ IMPORT_PATTERN = %r{^<<<\s+@/([^\s{#]+)(?:#([\w-]+))?(?:\{([^}]+)\})?\s*$}
22
+
23
+ self.priority = 1
24
+
25
+ def preprocess(content)
26
+ @docs_root = context[:docs_root] || "docs"
27
+ content.gsub(IMPORT_PATTERN) { |_| process_import(Regexp.last_match) }
28
+ end
29
+
30
+ private
31
+
32
+ def process_import(match)
33
+ filepath = match[1]
34
+ region = match[2]
35
+ options = match[3]
36
+
37
+ file_content = read_file(filepath)
38
+ return import_error(filepath, "File not found") unless file_content
39
+
40
+ file_content = extract_region(file_content, region) if region
41
+ return import_error(filepath, "Region '#{region}' not found") unless file_content
42
+
43
+ build_code_block(file_content, filepath, options)
44
+ end
45
+
46
+ def read_file(filepath)
47
+ full_path = File.join(@docs_root, filepath)
48
+ return nil unless File.exist?(full_path)
49
+
50
+ File.read(full_path)
51
+ end
52
+
53
+ def extract_region(content, region_name)
54
+ region_start = %r{^[ \t]*(?://|#|/\*)\s*#region\s+#{Regexp.escape(region_name)}\b.*$}
55
+ region_end = %r{^[ \t]*(?://|#|/\*|\*/)\s*#endregion\s*#{Regexp.escape(region_name)}?\b.*$}
56
+
57
+ lines = content.lines
58
+ start_index = lines.find_index { |line| line.match?(region_start) }
59
+ return nil unless start_index
60
+
61
+ end_index = lines[(start_index + 1)..].find_index { |line| line.match?(region_end) }
62
+ return nil unless end_index
63
+
64
+ end_index += start_index + 1
65
+ lines[(start_index + 1)...end_index].join
66
+ end
67
+
68
+ def build_code_block(content, filepath, options)
69
+ lang = detect_language(filepath)
70
+ highlights = nil
71
+
72
+ if options
73
+ parsed = parse_options(options)
74
+ lang = parsed[:lang] if parsed[:lang]
75
+ highlights = parsed[:highlights] if parsed[:highlights]
76
+ end
77
+
78
+ content = extract_line_range(content, highlights) if highlights&.include?("-") && !highlights.include?(",")
79
+
80
+ meta = build_meta_string(highlights, filepath)
81
+
82
+ "```#{lang}#{meta}\n#{content.chomp}\n```"
83
+ end
84
+
85
+ def parse_options(options)
86
+ parts = options.strip.split(/\s+/)
87
+ result = { highlights: nil, lang: nil }
88
+
89
+ parts.each do |part|
90
+ if part.match?(/^[\d,-]+$/)
91
+ result[:highlights] = part
92
+ else
93
+ result[:lang] = part
94
+ end
95
+ end
96
+
97
+ result
98
+ end
99
+
100
+ def extract_line_range(content, range_str)
101
+ return content unless range_str&.match?(/^\d+-\d+$/)
102
+
103
+ start_line, end_line = range_str.split("-").map(&:to_i)
104
+ lines = content.lines
105
+ lines[(start_line - 1)..(end_line - 1)]&.join || content
106
+ end
107
+
108
+ def build_meta_string(highlights, filepath)
109
+ parts = []
110
+ parts << " [#{File.basename(filepath)}]" if filepath
111
+ parts << " {#{highlights}}" if highlights && !highlights.match?(/^\d+-\d+$/)
112
+ parts.join
113
+ end
114
+
115
+ def detect_language(filepath)
116
+ ext = File.extname(filepath).delete_prefix(".")
117
+ EXTENSION_MAP[ext] || ext
118
+ end
119
+
120
+ def import_error(filepath, message)
121
+ "```\nError importing #{filepath}: #{message}\n```"
122
+ end
123
+ end
124
+ end
125
+ end
@@ -11,15 +11,15 @@ module Docyard
11
11
  @processors.sort_by! { |p| p.priority || 100 }
12
12
  end
13
13
 
14
- def run_preprocessors(content)
14
+ def run_preprocessors(content, context = {})
15
15
  @processors.reduce(content) do |processed_content, processor_class|
16
- processor_class.new.preprocess(processed_content)
16
+ processor_class.new(context).preprocess(processed_content)
17
17
  end
18
18
  end
19
19
 
20
- def run_postprocessors(html)
20
+ def run_postprocessors(html, context = {})
21
21
  @processors.reduce(html) do |processed_html, processor_class|
22
- processor_class.new.postprocess(processed_html)
22
+ processor_class.new(context).postprocess(processed_html)
23
23
  end
24
24
  end
25
25
 
@@ -7,7 +7,7 @@ module Docyard
7
7
 
8
8
  def postprocess(html)
9
9
  headings = extract_headings(html)
10
- Thread.current[:docyard_toc] = headings
10
+ context[:toc] = headings
11
11
  html
12
12
  end
13
13
 
@@ -1,8 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "code_block_feature_extractor"
4
+ require_relative "code_block_icon_detector"
5
+ require_relative "code_block_line_wrapper"
3
6
  require_relative "icon_detector"
7
+ require_relative "../icons"
8
+ require_relative "../renderer"
4
9
  require "kramdown"
5
10
  require "kramdown-parser-gfm"
11
+ require "cgi"
6
12
 
7
13
  module Docyard
8
14
  module Components
@@ -30,16 +36,27 @@ module Docyard
30
36
  def parse_section(section)
31
37
  return nil if section.strip.empty?
32
38
 
33
- parts = section.split("\n", 2)
34
- tab_name = parts[0]&.strip
39
+ tab_name, tab_content = extract_tab_parts(section)
35
40
  return nil if tab_name.nil? || tab_name.empty?
36
41
 
37
- tab_content = parts[1]&.strip || ""
42
+ build_tab_data(tab_name, tab_content)
43
+ end
44
+
45
+ def extract_tab_parts(section)
46
+ parts = section.split("\n", 2)
47
+ [parts[0]&.strip, parts[1]&.strip || ""]
48
+ end
49
+
50
+ def build_tab_data(tab_name, tab_content)
38
51
  icon_data = IconDetector.detect(tab_name, tab_content)
39
52
 
53
+ extracted = CodeBlockFeatureExtractor.process_markdown(tab_content)
54
+ rendered_content = render_markdown(extracted[:cleaned_markdown])
55
+ enhanced_content = enhance_code_blocks(rendered_content, extracted[:blocks])
56
+
40
57
  {
41
58
  name: icon_data[:name],
42
- content: render_markdown(tab_content),
59
+ content: enhanced_content,
43
60
  icon: icon_data[:icon],
44
61
  icon_source: icon_data[:icon_source]
45
62
  }
@@ -55,6 +72,120 @@ module Docyard
55
72
  syntax_highlighter: "rouge"
56
73
  ).to_html
57
74
  end
75
+
76
+ def enhance_code_blocks(html, blocks)
77
+ return html unless html.include?('<div class="highlight">')
78
+
79
+ block_index = 0
80
+ html.gsub(%r{<div class="highlight">(.*?)</div>}m) do
81
+ block_data = blocks[block_index] || {}
82
+ block_index += 1
83
+ render_enhanced_code_block(Regexp.last_match, block_data)
84
+ end
85
+ end
86
+
87
+ def render_enhanced_code_block(match, block_data)
88
+ original_html = match[0]
89
+ inner_html = match[1]
90
+ code_text = extract_code_text(inner_html)
91
+
92
+ processed_html = if needs_line_wrapping?(block_data)
93
+ wrap_code_block_lines(original_html, block_data)
94
+ else
95
+ original_html
96
+ end
97
+
98
+ Renderer.new.render_partial("_code_block", build_full_locals(processed_html, code_text, block_data))
99
+ end
100
+
101
+ def needs_line_wrapping?(block_data)
102
+ %i[highlights diff_lines focus_lines error_lines warning_lines].any? do |key|
103
+ block_data[key]&.any?
104
+ end
105
+ end
106
+
107
+ def wrap_code_block_lines(html, block_data)
108
+ wrapper_data = {
109
+ highlights: block_data[:highlights] || [],
110
+ diff_lines: block_data[:diff_lines] || {},
111
+ focus_lines: block_data[:focus_lines] || {},
112
+ error_lines: block_data[:error_lines] || {},
113
+ warning_lines: block_data[:warning_lines] || {},
114
+ start_line: extract_start_line(block_data[:option])
115
+ }
116
+ CodeBlockLineWrapper.wrap_code_block(html, wrapper_data)
117
+ end
118
+
119
+ def build_full_locals(processed_html, code_text, block_data)
120
+ title_data = CodeBlockIconDetector.detect(block_data[:title], block_data[:lang])
121
+ show_line_numbers = line_numbers_enabled?(block_data[:option])
122
+ start_line = extract_start_line(block_data[:option])
123
+
124
+ base_locals(processed_html, code_text, show_line_numbers, start_line)
125
+ .merge(feature_locals(block_data))
126
+ .merge(title_locals(title_data))
127
+ end
128
+
129
+ def base_locals(processed_html, code_text, show_line_numbers, start_line)
130
+ {
131
+ code_block_html: processed_html,
132
+ code_text: escape_html_attribute(code_text),
133
+ copy_icon: Icons.render("copy", "regular") || "",
134
+ show_line_numbers: show_line_numbers,
135
+ line_numbers: show_line_numbers ? generate_line_numbers(code_text, start_line) : [],
136
+ start_line: start_line
137
+ }
138
+ end
139
+
140
+ def feature_locals(block_data)
141
+ {
142
+ highlights: block_data[:highlights] || [],
143
+ diff_lines: block_data[:diff_lines] || {},
144
+ focus_lines: block_data[:focus_lines] || {},
145
+ error_lines: block_data[:error_lines] || {},
146
+ warning_lines: block_data[:warning_lines] || {}
147
+ }
148
+ end
149
+
150
+ def title_locals(title_data)
151
+ {
152
+ title: title_data[:title],
153
+ icon: title_data[:icon],
154
+ icon_source: title_data[:icon_source]
155
+ }
156
+ end
157
+
158
+ def line_numbers_enabled?(block_option)
159
+ return false if block_option == ":no-line-numbers"
160
+ return true if block_option&.start_with?(":line-numbers")
161
+
162
+ false
163
+ end
164
+
165
+ def extract_start_line(block_option)
166
+ return 1 unless block_option&.include?("=")
167
+
168
+ block_option.split("=").last.to_i
169
+ end
170
+
171
+ def generate_line_numbers(code_text, start_line)
172
+ line_count = code_text.lines.count
173
+ line_count = 1 if line_count.zero?
174
+ (start_line...(start_line + line_count)).to_a
175
+ end
176
+
177
+ def extract_code_text(html)
178
+ text = html.gsub(/<[^>]+>/, "")
179
+ text = CGI.unescapeHTML(text)
180
+ text.strip
181
+ end
182
+
183
+ def escape_html_attribute(text)
184
+ text.gsub('"', "&quot;")
185
+ .gsub("'", "&#39;")
186
+ .gsub("<", "&lt;")
187
+ .gsub(">", "&gt;")
188
+ end
58
189
  end
59
190
  end
60
191
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module TabsRangeFinder
6
+ module_function
7
+
8
+ def find_ranges(html)
9
+ ranges = []
10
+ start_pattern = '<div class="docyard-tabs"'
11
+
12
+ pos = 0
13
+ while (start_pos = html.index(start_pattern, pos))
14
+ end_pos = find_matching_close_div(html, start_pos)
15
+ ranges << (start_pos...end_pos) if end_pos
16
+ pos = end_pos || (start_pos + 1)
17
+ end
18
+ ranges
19
+ end
20
+
21
+ def find_matching_close_div(html, start_pos)
22
+ depth = 0
23
+ pos = start_pos
24
+
25
+ while pos < html.length
26
+ if html[pos, 4] == "<div"
27
+ depth += 1
28
+ pos += 4
29
+ elsif html[pos, 6] == "</div>"
30
+ depth -= 1
31
+ return pos + 6 if depth.zero?
32
+
33
+ pos += 6
34
+ else
35
+ pos += 1
36
+ end
37
+ end
38
+ nil
39
+ end
40
+ end
41
+ end
42
+ end
@@ -12,6 +12,7 @@ module Docyard
12
12
  validate_site_section
13
13
  validate_branding_section
14
14
  validate_build_section
15
+ validate_markdown_section
15
16
 
16
17
  raise ConfigError, format_errors if @errors.any?
17
18
  end
@@ -48,6 +49,13 @@ module Docyard
48
49
  validate_boolean(build["clean"], "build.clean")
49
50
  end
50
51
 
52
+ def validate_markdown_section
53
+ markdown = @config["markdown"]
54
+ return unless markdown
55
+
56
+ validate_boolean(markdown["lineNumbers"], "markdown.lineNumbers") if markdown.key?("lineNumbers")
57
+ end
58
+
51
59
  def validate_string(value, field_name)
52
60
  return if value.nil?
53
61
  return if value.is_a?(String)
@@ -34,6 +34,9 @@ module Docyard
34
34
  "prev_text" => "Previous",
35
35
  "next_text" => "Next"
36
36
  }
37
+ },
38
+ "markdown" => {
39
+ "lineNumbers" => false
37
40
  }
38
41
  }.freeze
39
42
 
@@ -74,6 +77,10 @@ module Docyard
74
77
  @navigation ||= ConfigSection.new(data["navigation"])
75
78
  end
76
79
 
80
+ def markdown
81
+ @markdown ||= ConfigSection.new(data["markdown"])
82
+ end
83
+
77
84
  private
78
85
 
79
86
  def load_config_data