docyard 1.3.0 → 1.4.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4892fa130361d02f347660d81932135f00c71d339c28b292e7bfecbe46ededa0
4
- data.tar.gz: 4697d95492f675b935db0dad134c11eb0d57f7aca611dd25c3e4e22bded9f031
3
+ metadata.gz: c34625ed1c0aee98891a94ac6d144f9660aa8493492e7f2a20dec2a344e38103
4
+ data.tar.gz: 271198813cf0eb7f88154976c927d47ff521ce5ba365cb0c335d6976f18d1eda
5
5
  SHA512:
6
- metadata.gz: 342e2e7f16c32da9da273b6325532ad8ee842d58304faf309a0e634be5f7c2d3a6a8c5eb0176b09e0b518a67150c25223b5ad3be9c67da90e974e4be9871e28e
7
- data.tar.gz: 2f1b65cde20d044348908884fbfd0a185d47d6ef73d9fdd516e5f11792262700550bb70d6ec7523dc910dcaed864d2df51bf6f54480997092c63dfe926b6629d
6
+ metadata.gz: '092ac5fb44e695fc1271eeb96e8f781d3258303431ea57dee56b3f7ceb6a8b83b54ba5e5000c8ea12d5fbc3eb99cb22fed380ca9b14de2b3408abf4645ff77e7'
7
+ data.tar.gz: 68ac38bb44491ebf564a489c3d14a4ba2ea945bd0b62cc2dc9433178f97e5961a01526945087a8b9778f3deaef37e6c7481a4ec903172a741b34f04a96bffe73
data/CHANGELOG.md CHANGED
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.1] - 2026-11-03
11
+
12
+ ### Fixed
13
+ - `robots.txt` now uses an absolute Sitemap URL when `url` is configured (#165, #166)
14
+ - `sitemap.xml` entries now use absolute URLs when `url` is configured, as required by the sitemaps protocol (#169)
15
+
16
+ ### Changed
17
+ - Widened dependency ranges to support newer major versions: `rouge` (< 6), `puma` (< 9), and `parallel` (< 3) (#159, #161, #162)
18
+
19
+ ## [1.4.0] - 2026-02-22
20
+
21
+ ### Added
22
+ - **Code Annotations** - Clickable inline markers in code blocks with popover explanations using `// (1)` syntax and a matching ordered list (#156)
23
+ - **Variables** - Template variables with `{{ variable }}` syntax, defined in `docyard.yml` under `variables:` (#155)
24
+
25
+ ### Documentation
26
+ - Added Code Annotations section to Code Blocks page
27
+ - Added Variables page with usage examples and configuration reference
28
+
10
29
  ## [1.3.0] - 2026-02-17
11
30
 
12
31
  ### Added
@@ -275,7 +294,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
275
294
  - Initial gem structure
276
295
  - Project scaffolding
277
296
 
278
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.3.0...HEAD
297
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.4.0...HEAD
298
+ [1.4.0]: https://github.com/sanifhimani/docyard/compare/v1.3.0...v1.4.0
279
299
  [1.3.0]: https://github.com/sanifhimani/docyard/compare/v1.2.0...v1.3.0
280
300
  [1.2.0]: https://github.com/sanifhimani/docyard/compare/v1.1.0...v1.2.0
281
301
  [1.1.0]: https://github.com/sanifhimani/docyard/compare/v1.0.2...v1.1.0
@@ -36,8 +36,8 @@ module Docyard
36
36
  end
37
37
 
38
38
  def build_sitemap(urls)
39
- base = config.build.base
40
- base = base.chop if base.end_with?("/")
39
+ base = config.url || config.build.base
40
+ base = base.chomp("/")
41
41
 
42
42
  xml = ['<?xml version="1.0" encoding="UTF-8"?>']
43
43
  xml << '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">'
@@ -141,14 +141,14 @@ module Docyard
141
141
  end
142
142
 
143
143
  def robots_txt_content
144
- base = config.build.base
145
- base = "#{base}/" unless base.end_with?("/")
144
+ base = config.url || config.build.base
145
+ base = base.chomp("/")
146
146
 
147
147
  <<~ROBOTS
148
148
  User-agent: *
149
149
  Allow: /
150
150
 
151
- Sitemap: #{base}sitemap.xml
151
+ Sitemap: #{base}/sitemap.xml
152
152
  ROBOTS
153
153
  end
154
154
  end
@@ -10,6 +10,7 @@ module Docyard
10
10
  CalloutProcessor = Processors::CalloutProcessor
11
11
  CodeBlockProcessor = Processors::CodeBlockProcessor
12
12
  CodeGroupProcessor = Processors::CodeGroupProcessor
13
+ CodeBlockAnnotationPreprocessor = Processors::CodeBlockAnnotationPreprocessor
13
14
  CodeBlockDiffPreprocessor = Processors::CodeBlockDiffPreprocessor
14
15
  CodeBlockFocusPreprocessor = Processors::CodeBlockFocusPreprocessor
15
16
  CodeBlockOptionsPreprocessor = Processors::CodeBlockOptionsPreprocessor
@@ -25,6 +26,7 @@ module Docyard
25
26
  TableWrapperProcessor = Processors::TableWrapperProcessor
26
27
  TabsProcessor = Processors::TabsProcessor
27
28
  TooltipProcessor = Processors::TooltipProcessor
29
+ VariablesProcessor = Processors::VariablesProcessor
28
30
 
29
31
  CodeBlockFeatureExtractor = Support::CodeBlock::FeatureExtractor
30
32
  CodeBlockLineWrapper = Support::CodeBlock::LineWrapper
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "kramdown"
4
+ require "kramdown-parser-gfm"
5
+ require_relative "../base_processor"
6
+ require_relative "../support/code_block/patterns"
7
+ require_relative "../support/code_block/annotation_list_parser"
8
+
9
+ module Docyard
10
+ module Components
11
+ module Processors
12
+ class CodeBlockAnnotationPreprocessor < BaseProcessor
13
+ include Support::CodeBlock::Patterns
14
+
15
+ self.priority = 8
16
+
17
+ CODE_BLOCK_REGEX = /^```(\w*).*?\n(.*?)^```/m
18
+ TABS_BLOCK_REGEX = /^:::tabs[ \t]*\n.*?^:::[ \t]*$/m
19
+ CODE_GROUP_REGEX = /^:::code-group[ \t]*\n.*?^:::[ \t]*$/m
20
+
21
+ ListParser = Support::CodeBlock::AnnotationListParser
22
+
23
+ def preprocess(content)
24
+ initialize_state
25
+ @skip_ranges = find_skip_ranges(content)
26
+ process_content(content)
27
+ end
28
+
29
+ private
30
+
31
+ def initialize_state
32
+ context[:code_block_annotation_markers] ||= []
33
+ context[:code_block_annotation_content] ||= []
34
+ @block_index = 0
35
+ end
36
+
37
+ def process_content(content)
38
+ result = +""
39
+ last_end = 0
40
+
41
+ content.scan(CODE_BLOCK_REGEX) do
42
+ match = Regexp.last_match
43
+ next if match.begin(0) < last_end
44
+
45
+ result << content[last_end...match.begin(0)]
46
+ processed, new_end = process_match(match, content)
47
+ result << processed
48
+ last_end = new_end
49
+ end
50
+
51
+ result << content[last_end..]
52
+ end
53
+
54
+ def process_match(match, content)
55
+ if inside_skip_range?(match.begin(0))
56
+ [match[0], match.end(0)]
57
+ else
58
+ process_annotated_block(match, content)
59
+ end
60
+ end
61
+
62
+ def process_annotated_block(match, content)
63
+ markers = extract_annotation_markers(match[2])
64
+ code_end = match.end(0)
65
+
66
+ if markers.any?
67
+ process_with_list(match, content, markers, code_end)
68
+ else
69
+ store_empty_markers
70
+ @block_index += 1
71
+ [match[0], code_end]
72
+ end
73
+ end
74
+
75
+ def process_with_list(match, content, markers, code_end)
76
+ list_result = ListParser.find_after_code_block(content, code_end)
77
+
78
+ if list_result
79
+ store_markers_and_content(markers, list_result[:items])
80
+ cleaned_code = strip_annotation_markers(match[2])
81
+ @block_index += 1
82
+ ["#{match[0].sub(match[2], cleaned_code)}\n", list_result[:end_position]]
83
+ else
84
+ store_empty_markers
85
+ @block_index += 1
86
+ [match[0], code_end]
87
+ end
88
+ end
89
+
90
+ def store_markers_and_content(markers, list_items)
91
+ context[:code_block_annotation_markers][@block_index] = markers
92
+ context[:code_block_annotation_content][@block_index] = render_annotation_content(list_items)
93
+ end
94
+
95
+ def store_empty_markers
96
+ context[:code_block_annotation_markers][@block_index] = {}
97
+ context[:code_block_annotation_content][@block_index] = {}
98
+ end
99
+
100
+ def extract_annotation_markers(code_content)
101
+ markers = {}
102
+ code_content.lines.each_with_index do |line, index|
103
+ next unless (match = line.match(ANNOTATION_MARKER_PATTERN))
104
+
105
+ num = match.captures.compact.first.to_i
106
+ markers[index + 1] = num
107
+ end
108
+ markers
109
+ end
110
+
111
+ def strip_annotation_markers(code_content)
112
+ code_content.lines.map { |line| strip_single_marker(line) }.join
113
+ end
114
+
115
+ def strip_single_marker(line)
116
+ return line unless line.match?(ANNOTATION_MARKER_PATTERN)
117
+
118
+ stripped = line.sub(ANNOTATION_MARKER_PATTERN, "")
119
+ stripped.end_with?("\n") ? stripped : "#{stripped}\n"
120
+ end
121
+
122
+ def render_annotation_content(list_items)
123
+ list_items.transform_values { |markdown_text| render_markdown(markdown_text) }
124
+ end
125
+
126
+ def render_markdown(text)
127
+ Kramdown::Document.new(text, input: "GFM", hard_wrap: false).to_html.strip
128
+ end
129
+
130
+ def inside_skip_range?(position)
131
+ @skip_ranges.any? { |range| range.cover?(position) }
132
+ end
133
+
134
+ def find_skip_ranges(content)
135
+ find_ranges(content, TABS_BLOCK_REGEX) + find_ranges(content, CODE_GROUP_REGEX)
136
+ end
137
+
138
+ def find_ranges(content, pattern)
139
+ ranges = []
140
+ content.scan(pattern) do
141
+ match = Regexp.last_match
142
+ ranges << (match.begin(0)...match.end(0))
143
+ end
144
+ ranges
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -33,11 +33,17 @@ module Docyard
33
33
  def initialize_postprocess_state(html)
34
34
  @block_index = 0
35
35
  @options = context[:code_block_options] || []
36
+ initialize_line_feature_state
37
+ @tabs_ranges = TabsRangeFinder.find_ranges(html)
38
+ end
39
+
40
+ def initialize_line_feature_state
36
41
  @diff_lines = context[:code_block_diff_lines] || []
37
42
  @focus_lines = context[:code_block_focus_lines] || []
38
43
  @error_lines = context[:code_block_error_lines] || []
39
44
  @warning_lines = context[:code_block_warning_lines] || []
40
- @tabs_ranges = TabsRangeFinder.find_ranges(html)
45
+ @annotation_markers = context[:code_block_annotation_markers] || []
46
+ @annotation_content = context[:code_block_annotation_content] || []
41
47
  end
42
48
 
43
49
  def process_all_highlight_blocks(html)
@@ -99,25 +105,28 @@ module Docyard
99
105
 
100
106
  def build_block_data(code_text, opts, show_line_numbers, start_line, title_data)
101
107
  {
102
- text: code_text,
103
- highlights: opts[:highlights],
108
+ text: code_text, highlights: opts[:highlights],
109
+ show_line_numbers: show_line_numbers, start_line: start_line,
110
+ line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
111
+ title: title_data[:title], icon: title_data[:icon], icon_source: title_data[:icon_source]
112
+ }.merge(current_line_features)
113
+ end
114
+
115
+ def current_line_features
116
+ {
104
117
  diff_lines: @diff_lines[@block_index] || {},
105
118
  focus_lines: @focus_lines[@block_index] || {},
106
119
  error_lines: @error_lines[@block_index] || {},
107
120
  warning_lines: @warning_lines[@block_index] || {},
108
- show_line_numbers: show_line_numbers,
109
- line_numbers: show_line_numbers ? LineNumbers.generate_numbers(code_text, start_line) : [],
110
- start_line: start_line,
111
- title: title_data[:title],
112
- icon: title_data[:icon],
113
- icon_source: title_data[:icon_source]
121
+ annotation_markers: @annotation_markers[@block_index] || {},
122
+ annotation_content: @annotation_content[@block_index] || {}
114
123
  }
115
124
  end
116
125
 
117
126
  def process_html_for_highlighting(original_html, block_data)
118
127
  needs_wrapping = block_data[:highlights].any? || block_data[:diff_lines].any? ||
119
128
  block_data[:focus_lines].any? || block_data[:error_lines].any? ||
120
- block_data[:warning_lines].any?
129
+ block_data[:warning_lines].any? || block_data[:annotation_markers].any?
121
130
  return original_html unless needs_wrapping
122
131
 
123
132
  wrap_code_block(original_html, block_data)
@@ -150,7 +159,8 @@ module Docyard
150
159
  def line_feature_locals(block_data)
151
160
  { highlights: block_data[:highlights], diff_lines: block_data[:diff_lines],
152
161
  focus_lines: block_data[:focus_lines], error_lines: block_data[:error_lines],
153
- warning_lines: block_data[:warning_lines] }
162
+ warning_lines: block_data[:warning_lines], annotation_markers: block_data[:annotation_markers],
163
+ annotation_content: block_data[:annotation_content] }
154
164
  end
155
165
 
156
166
  def title_locals(block_data)
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base_processor"
4
+ require_relative "../support/markdown_code_block_helper"
5
+
6
+ module Docyard
7
+ module Components
8
+ module Processors
9
+ class VariablesProcessor < BaseProcessor
10
+ include Support::MarkdownCodeBlockHelper
11
+
12
+ VARIABLE_PATTERN = /\{\{\s*([a-zA-Z0-9_.]+)\s*\}\}/
13
+ VARS_SUFFIX_PATTERN = /^(`{3,}|~{3,})(\S+)-vars(.*)/
14
+
15
+ self.priority = 1
16
+
17
+ def preprocess(content)
18
+ variables = context.dig(:config, "variables") || {}
19
+ return content if variables.empty?
20
+
21
+ segments = split_by_code_blocks(content)
22
+ segments.map { |segment| process_segment(segment, variables) }.join
23
+ end
24
+
25
+ private
26
+
27
+ def process_segment(segment, variables)
28
+ return substitute_variables(segment[:content], variables) if segment[:type] == :text
29
+
30
+ match = segment[:content].match(VARS_SUFFIX_PATTERN)
31
+ return segment[:content] unless match
32
+
33
+ stripped = segment[:content].sub("#{match[2]}-vars", match[2])
34
+ substitute_variables(stripped, variables)
35
+ end
36
+
37
+ def substitute_variables(content, variables)
38
+ content.gsub(VARIABLE_PATTERN) do |original|
39
+ key = Regexp.last_match(1)
40
+ value = resolve_variable(key, variables)
41
+ value.nil? ? original : value.to_s
42
+ end
43
+ end
44
+
45
+ def resolve_variable(key, variables)
46
+ keys = key.split(".")
47
+ keys.reduce(variables) do |current, k|
48
+ return nil unless current.is_a?(Hash)
49
+
50
+ current[k]
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Docyard
4
+ module Components
5
+ module Support
6
+ module CodeBlock
7
+ module AnnotationListParser
8
+ ORDERED_LIST_ITEM = /\A(\d+)\.\s+(.*)/
9
+ CONTINUATION_LINE = /\A\s{2,}(\S.*)/
10
+ BLANK_LINE = /\A\s*\z/
11
+ LIST_START = /\A(\s*\n)*(\d+\.\s+)/m
12
+
13
+ module_function
14
+
15
+ def parse(text)
16
+ state = { items: {}, current_num: nil, current_lines: [] }
17
+ catch(:done) { text.each_line { |line| consume_line(state, line) } }
18
+ finalize(state)
19
+ state[:items]
20
+ end
21
+
22
+ def find_after_code_block(content, position)
23
+ rest = content[position..]
24
+ return nil unless rest
25
+
26
+ preamble = rest.match(LIST_START)
27
+ return nil unless preamble
28
+
29
+ list_start = preamble.begin(2)
30
+ list_text = rest[list_start..]
31
+ parsed = parse_with_extent(list_text)
32
+ return nil if parsed[:items].empty?
33
+
34
+ { text: parsed[:consumed], end_position: position + list_start + parsed[:length], items: parsed[:items] }
35
+ end
36
+
37
+ def parse_with_extent(text)
38
+ state = { items: {}, current_num: nil, current_lines: [] }
39
+ consumed_length = 0
40
+
41
+ catch(:done) do
42
+ text.each_line do |line|
43
+ consume_line(state, line)
44
+ consumed_length += line.length
45
+ end
46
+ end
47
+
48
+ finalize(state)
49
+ { items: state[:items], consumed: text[0...consumed_length], length: consumed_length }
50
+ end
51
+
52
+ def consume_line(state, line)
53
+ if (match = line.match(ORDERED_LIST_ITEM))
54
+ start_new_item(state, match)
55
+ elsif state[:current_num] && (cont = line.match(CONTINUATION_LINE))
56
+ state[:current_lines] << cont[1].rstrip
57
+ elsif state[:current_num] && line.match?(BLANK_LINE)
58
+ state[:current_lines] << ""
59
+ else
60
+ finalize(state)
61
+ throw :done
62
+ end
63
+ end
64
+
65
+ def start_new_item(state, match)
66
+ finalize(state)
67
+ state[:current_num] = match[1].to_i
68
+ state[:current_lines] = [match[2].rstrip]
69
+ end
70
+
71
+ def finalize(state)
72
+ return unless state[:current_num]
73
+
74
+ state[:items][state[:current_num]] = state[:current_lines].join("\n").strip
75
+ state[:current_num] = nil
76
+ state[:current_lines] = []
77
+ end
78
+
79
+ private_class_method :consume_line, :start_new_item, :finalize, :parse_with_extent
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -24,10 +24,18 @@ module Docyard
24
24
  source_line = index + 1
25
25
  display_line = block_data[:start_line] + index
26
26
  classes = build_line_classes(source_line, display_line, block_data)
27
- %(<span class="#{classes}">#{line}</span>)
27
+ annotation = annotation_badge(source_line, block_data)
28
+ line_with_badge = insert_before_newline(line, annotation)
29
+ %(<span class="#{classes}">#{line_with_badge}</span>)
28
30
  end
29
31
  end
30
32
 
33
+ def insert_before_newline(line, annotation)
34
+ return line if annotation.empty?
35
+
36
+ line.end_with?("\n") ? "#{line.chomp}#{annotation}\n" : "#{line}#{annotation}"
37
+ end
38
+
31
39
  DIFF_CLASSES = { addition: "docyard-code-line--diff-add", deletion: "docyard-code-line--diff-remove" }.freeze
32
40
 
33
41
  def build_line_classes(source_line, display_line, block_data)
@@ -43,6 +51,22 @@ module Docyard
43
51
  ("docyard-code-line--warning" if block_data[:warning_lines][source_line])
44
52
  ].compact
45
53
  end
54
+
55
+ ANNOTATION_ICON = "<i class=\"ph ph-plus-circle\" aria-hidden=\"true\"></i>"
56
+
57
+ def annotation_badge(source_line, block_data)
58
+ markers = block_data[:annotation_markers] || {}
59
+ num = markers[source_line]
60
+ return "" unless num
61
+
62
+ content = (block_data[:annotation_content] || {})[num]
63
+ return "" unless content
64
+
65
+ escaped = content.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;").gsub('"', "&quot;")
66
+ %(<button class="docyard-code-annotation" data-annotation-id="#{num}") +
67
+ %( data-annotation-content="#{escaped}" type="button" aria-label="Annotation #{num}">) +
68
+ %(#{ANNOTATION_ICON}</button>)
69
+ end
46
70
  end
47
71
  end
48
72
  end
@@ -48,6 +48,17 @@ module Docyard
48
48
  ;\s*\[!code\s+warning\]
49
49
  )[^\S\n]*
50
50
  }x
51
+
52
+ ANNOTATION_MARKER_PATTERN = %r{
53
+ (?:
54
+ //\s*\((\d+)\)\s*$ |
55
+ \#\s*\((\d+)\)\s*$ |
56
+ /\*\s*\((\d+)\)\s*\*/\s*$ |
57
+ --\s*\((\d+)\)\s*$ |
58
+ <!--\s*\((\d+)\)\s*-->\s*$ |
59
+ ;\s*\((\d+)\)\s*$
60
+ )
61
+ }x
51
62
  end
52
63
  end
53
64
  end
@@ -23,7 +23,8 @@ module Docyard
23
23
  repo: REPO_SCHEMA,
24
24
  analytics: ANALYTICS_SCHEMA,
25
25
  feedback: FEEDBACK_SCHEMA,
26
- social_cards: SOCIAL_CARDS_SCHEMA
26
+ social_cards: SOCIAL_CARDS_SCHEMA,
27
+ variables: { type: :hash, allow_extra_keys: true, keys: {} }
27
28
  }.freeze
28
29
  end
29
30
  end
@@ -27,7 +27,8 @@ module Docyard
27
27
  "last_updated" => true },
28
28
  "analytics" => { "google" => nil, "plausible" => nil, "fathom" => nil, "script" => nil },
29
29
  "feedback" => { "enabled" => false, "question" => "Was this page helpful?" },
30
- "social_cards" => { "enabled" => false }
30
+ "social_cards" => { "enabled" => false },
31
+ "variables" => {}
31
32
  }.freeze
32
33
 
33
34
  attr_reader :data, :file_path
@@ -65,6 +66,7 @@ module Docyard
65
66
  def analytics = @analytics ||= Section.new(data["analytics"])
66
67
  def feedback = @feedback ||= Section.new(data["feedback"])
67
68
  def social_cards = @social_cards ||= Section.new(data["social_cards"])
69
+ def variables = data["variables"]
68
70
 
69
71
  def announcement
70
72
  @announcement ||= data["announcement"] ? Section.new(data["announcement"]) : nil
@@ -170,7 +170,7 @@ module Docyard
170
170
 
171
171
  def normalize_options(options)
172
172
  return {} if options.nil?
173
- return options if options.is_a?(Hash) && options.keys.all? { |k| k.is_a?(Symbol) }
173
+ return options if options.is_a?(Hash) && options.keys.all?(Symbol)
174
174
 
175
175
  options.transform_keys(&:to_sym)
176
176
  end
@@ -19,6 +19,7 @@ require_relative "../components/processors/include_processor"
19
19
  require_relative "../components/processors/code_block_options_preprocessor"
20
20
  require_relative "../components/processors/code_block_diff_preprocessor"
21
21
  require_relative "../components/processors/code_block_focus_preprocessor"
22
+ require_relative "../components/processors/code_block_annotation_preprocessor"
22
23
  require_relative "../components/processors/code_block_extended_fence_preprocessor"
23
24
  require_relative "../components/processors/code_block_extended_fence_postprocessor"
24
25
  require_relative "../components/processors/table_wrapper_processor"
@@ -29,6 +30,7 @@ require_relative "../components/processors/video_embed_processor"
29
30
  require_relative "../components/processors/file_tree_processor"
30
31
  require_relative "../components/processors/abbreviation_processor"
31
32
  require_relative "../components/processors/tooltip_processor"
33
+ require_relative "../components/processors/variables_processor"
32
34
  require_relative "../components/processors/table_of_contents_processor"
33
35
  require_relative "../components/aliases"
34
36
 
@@ -0,0 +1,265 @@
1
+ .docyard-code-annotation {
2
+ position: relative;
3
+ display: inline-flex;
4
+ align-items: center;
5
+ justify-content: center;
6
+ width: var(--code-annotation-size);
7
+ height: var(--code-annotation-size);
8
+ margin-left: var(--spacing-1);
9
+ padding: 0;
10
+ border: none;
11
+ border-radius: var(--radius-4xl);
12
+ background: none;
13
+ color: var(--primary);
14
+ cursor: pointer;
15
+ vertical-align: middle;
16
+ opacity: 0.7;
17
+ transition: opacity 150ms ease, transform 150ms ease;
18
+ }
19
+
20
+ .docyard-code-annotation::before {
21
+ content: "";
22
+ position: absolute;
23
+ inset: -0.375em;
24
+ }
25
+
26
+ .docyard-code-annotation i[class*="ph-"] {
27
+ font-size: 1.25em;
28
+ vertical-align: 0;
29
+ }
30
+
31
+ @media (hover: hover) {
32
+ .docyard-code-annotation:hover {
33
+ opacity: 1;
34
+ }
35
+ }
36
+
37
+ .docyard-code-annotation:active {
38
+ transform: scale(0.92);
39
+ }
40
+
41
+ .docyard-code-annotation.is-active {
42
+ opacity: 1;
43
+ }
44
+
45
+ .docyard-code-annotation:focus-visible {
46
+ opacity: 1;
47
+ outline: 1.5px solid var(--primary);
48
+ outline-offset: 1px;
49
+ border-radius: 2px;
50
+ }
51
+
52
+ .docyard-code-block--has-annotations .docyard-code-line {
53
+ padding-right: calc(var(--code-annotation-size) + var(--spacing-1));
54
+ }
55
+
56
+ .docyard-code-annotation-popover {
57
+ position: absolute;
58
+ z-index: var(--z-tooltip);
59
+ width: max-content;
60
+ max-width: 24rem;
61
+ max-height: 20rem;
62
+ overflow-y: auto;
63
+ padding: var(--spacing-3) var(--spacing-4);
64
+ background: var(--popover);
65
+ color: oklch(from var(--popover-foreground) l c h / 85%);
66
+ border: 1px solid var(--border);
67
+ border-radius: var(--radius-lg);
68
+ box-shadow:
69
+ 0 1px 2px oklch(from var(--foreground) l c h / 3%),
70
+ 0 4px 12px oklch(from var(--foreground) l c h / 6%),
71
+ 0 16px 32px -8px oklch(from var(--foreground) l c h / 8%);
72
+ pointer-events: none;
73
+ opacity: 0;
74
+ transform: translateY(4px) scale(0.96);
75
+ transform-origin: var(--arrow-left, 16px) top;
76
+ transition:
77
+ opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
78
+ transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
79
+ }
80
+
81
+ .docyard-code-annotation-popover.is-visible {
82
+ opacity: 1;
83
+ transform: translateY(0) scale(1);
84
+ pointer-events: auto;
85
+ transition:
86
+ opacity 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94),
87
+ transform 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94);
88
+ }
89
+
90
+ .docyard-code-annotation-popover.is-above {
91
+ transform: translateY(-4px) scale(0.96);
92
+ transform-origin: var(--arrow-left, 16px) bottom;
93
+ }
94
+
95
+ .docyard-code-annotation-popover.is-above.is-visible {
96
+ transform: translateY(0) scale(1);
97
+ }
98
+
99
+ .docyard-code-annotation-popover::after {
100
+ content: "";
101
+ position: absolute;
102
+ top: -5px;
103
+ left: var(--arrow-left, 16px);
104
+ width: 9px;
105
+ height: 9px;
106
+ background: var(--popover);
107
+ border-top: 1px solid var(--border);
108
+ border-left: 1px solid var(--border);
109
+ border-radius: 1px 0 0 0;
110
+ transform: rotate(45deg);
111
+ }
112
+
113
+ .docyard-code-annotation-popover.is-above::after {
114
+ top: auto;
115
+ bottom: -5px;
116
+ border-top: none;
117
+ border-left: none;
118
+ border-bottom: 1px solid var(--border);
119
+ border-right: 1px solid var(--border);
120
+ }
121
+
122
+ .docyard-code-annotation-popover p {
123
+ margin: 0;
124
+ font-size: var(--text-sm);
125
+ line-height: var(--leading-relaxed);
126
+ }
127
+
128
+ .docyard-code-annotation-popover * + p,
129
+ .docyard-code-annotation-popover * + ul,
130
+ .docyard-code-annotation-popover * + ol,
131
+ .docyard-code-annotation-popover * + blockquote {
132
+ margin-top: var(--spacing-2);
133
+ }
134
+
135
+ .docyard-code-annotation-popover * + .table-wrapper,
136
+ .docyard-code-annotation-popover * + table {
137
+ margin-top: var(--spacing-2);
138
+ }
139
+
140
+ .docyard-code-annotation-popover ul,
141
+ .docyard-code-annotation-popover ol {
142
+ margin: 0;
143
+ padding-left: var(--spacing-6);
144
+ }
145
+
146
+ .docyard-code-annotation-popover ul {
147
+ list-style-type: disc;
148
+ }
149
+
150
+ .docyard-code-annotation-popover ol {
151
+ list-style-type: decimal;
152
+ }
153
+
154
+ .docyard-code-annotation-popover li {
155
+ margin: var(--spacing-1) 0;
156
+ padding-left: var(--spacing-1);
157
+ font-size: var(--text-sm);
158
+ line-height: var(--leading-relaxed);
159
+ }
160
+
161
+ .docyard-code-annotation-popover li::marker {
162
+ color: var(--muted-foreground);
163
+ }
164
+
165
+ .docyard-code-annotation-popover table {
166
+ width: 100%;
167
+ border-spacing: 0;
168
+ border-collapse: separate;
169
+ font-size: var(--text-sm);
170
+ margin: 0;
171
+ border: 1px solid var(--table-border);
172
+ border-radius: var(--radius-lg);
173
+ overflow: hidden;
174
+ }
175
+
176
+ .docyard-code-annotation-popover thead {
177
+ background-color: var(--table-header-bg);
178
+ }
179
+
180
+ .docyard-code-annotation-popover th {
181
+ padding: var(--spacing-2) var(--spacing-3);
182
+ text-align: left;
183
+ font-weight: var(--font-semibold);
184
+ font-size: var(--text-xs);
185
+ color: var(--popover-foreground);
186
+ vertical-align: middle;
187
+ }
188
+
189
+ .docyard-code-annotation-popover td {
190
+ padding: var(--spacing-2) var(--spacing-3);
191
+ font-size: var(--text-xs);
192
+ vertical-align: middle;
193
+ border-top: 1px solid var(--table-row-border);
194
+ }
195
+
196
+ .docyard-code-annotation-popover blockquote {
197
+ margin: 0;
198
+ padding-left: var(--spacing-6);
199
+ border-left: 4px solid var(--border);
200
+ color: var(--muted-foreground);
201
+ }
202
+
203
+ .docyard-code-annotation-popover blockquote p {
204
+ margin: var(--spacing-1) 0;
205
+ }
206
+
207
+ .docyard-code-annotation-popover code {
208
+ padding: 0.125rem 0.5rem;
209
+ background-color: oklch(from var(--muted) l c h / 80%);
210
+ color: var(--popover-foreground);
211
+ border-radius: var(--radius-sm);
212
+ font-size: 0.875em;
213
+ font-weight: var(--font-medium);
214
+ font-variant-ligatures: none;
215
+ }
216
+
217
+ .docyard-code-annotation-popover strong {
218
+ font-weight: var(--font-semibold);
219
+ color: var(--popover-foreground);
220
+ }
221
+
222
+ .docyard-code-annotation-popover a {
223
+ color: var(--popover-foreground);
224
+ font-weight: var(--font-semibold);
225
+ text-decoration: none;
226
+ border-bottom: 1px solid oklch(from var(--primary) l c h / 40%);
227
+ transition: border-bottom var(--transition-fast);
228
+ }
229
+
230
+ .docyard-code-annotation-popover a:hover {
231
+ border-bottom: 2px solid var(--primary);
232
+ }
233
+
234
+ @media (max-width: 640px) {
235
+ .docyard-code-annotation-popover {
236
+ max-width: 16rem;
237
+ padding: var(--spacing-2-5) var(--spacing-3);
238
+ }
239
+
240
+ .docyard-code-annotation-popover p,
241
+ .docyard-code-annotation-popover li {
242
+ font-size: var(--text-xs);
243
+ }
244
+ }
245
+
246
+ @media (prefers-reduced-motion: reduce) {
247
+ .docyard-code-annotation,
248
+ .docyard-code-annotation-popover {
249
+ transition: none;
250
+ }
251
+
252
+ .docyard-code-annotation:active {
253
+ transform: none;
254
+ }
255
+
256
+ .docyard-code-annotation-popover,
257
+ .docyard-code-annotation-popover.is-above {
258
+ transform: none;
259
+ }
260
+
261
+ .docyard-code-annotation-popover.is-visible,
262
+ .docyard-code-annotation-popover.is-above.is-visible {
263
+ transform: none;
264
+ }
265
+ }
@@ -84,6 +84,8 @@
84
84
  --callout-danger-background: rgba(254, 242, 242, 0.5);
85
85
  --callout-danger-border: rgba(239, 68, 68, 0.2);
86
86
 
87
+ --code-annotation-size: 1.25em;
88
+
87
89
  --code-block-bg: oklab(0 0 0 / 0.03);
88
90
  --code-block-border: oklab(0 0 0 / 0.06);
89
91
  --code-block-header-bg: oklab(0 0 0 / 0.02);
@@ -0,0 +1,136 @@
1
+ var annotationPopover = null;
2
+ var activeAnnotationButton = null;
3
+
4
+ function initCodeAnnotations(root) {
5
+ if (typeof root === 'undefined') root = document;
6
+ var buttons = root.querySelectorAll('.docyard-code-annotation');
7
+ if (buttons.length === 0) return;
8
+
9
+ if (!annotationPopover) {
10
+ annotationPopover = document.createElement('div');
11
+ annotationPopover.className = 'docyard-code-annotation-popover';
12
+ document.body.appendChild(annotationPopover);
13
+
14
+ document.addEventListener('click', function(e) {
15
+ if (activeAnnotationButton &&
16
+ !annotationPopover.contains(e.target) &&
17
+ !e.target.closest('.docyard-code-annotation')) {
18
+ hideAnnotationPopover();
19
+ }
20
+ });
21
+
22
+ document.addEventListener('keydown', function(e) {
23
+ if (e.key === 'Escape' && activeAnnotationButton) {
24
+ hideAnnotationPopover();
25
+ activeAnnotationButton.focus();
26
+ }
27
+ });
28
+ }
29
+
30
+ buttons.forEach(function(button) {
31
+ if (button.hasAttribute('data-annotation-initialized')) return;
32
+ button.setAttribute('data-annotation-initialized', 'true');
33
+
34
+ button.addEventListener('click', function(e) {
35
+ e.stopPropagation();
36
+ if (activeAnnotationButton === button) {
37
+ hideAnnotationPopover();
38
+ } else {
39
+ showAnnotationPopover(button);
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ function setAnnotationIcon(button, name, animate) {
46
+ var icon = button.querySelector('i[class*="ph-"]');
47
+ if (!icon) return;
48
+ if (!animate) {
49
+ icon.className = 'ph ph-' + name;
50
+ return;
51
+ }
52
+ icon.style.transition = 'opacity 100ms ease, transform 100ms ease';
53
+ icon.style.opacity = '0';
54
+ icon.style.transform = 'scale(0.5)';
55
+ setTimeout(function() {
56
+ icon.className = 'ph ph-' + name;
57
+ icon.style.opacity = '1';
58
+ icon.style.transform = 'scale(1)';
59
+ }, 100);
60
+ }
61
+
62
+ function showAnnotationPopover(button) {
63
+ if (activeAnnotationButton) {
64
+ setAnnotationIcon(activeAnnotationButton, 'plus-circle', true);
65
+ activeAnnotationButton.classList.remove('is-active');
66
+ }
67
+
68
+ activeAnnotationButton = button;
69
+ button.classList.add('is-active');
70
+ setAnnotationIcon(button, 'x-circle', true);
71
+ annotationPopover.innerHTML = button.getAttribute('data-annotation-content');
72
+
73
+ annotationPopover.classList.remove('is-above');
74
+ annotationPopover.style.visibility = 'hidden';
75
+ annotationPopover.classList.add('is-visible');
76
+
77
+ requestAnimationFrame(function() {
78
+ positionPopover(button);
79
+ });
80
+ }
81
+
82
+ function positionPopover(button) {
83
+ var rect = button.getBoundingClientRect();
84
+ var scrollX = window.scrollX;
85
+ var scrollY = window.scrollY;
86
+ var popoverRect = annotationPopover.getBoundingClientRect();
87
+ var gap = 6;
88
+ var padding = 12;
89
+ var viewportWidth = window.innerWidth;
90
+ var viewportHeight = window.innerHeight;
91
+
92
+ var left = rect.left + scrollX + (rect.width / 2) - (popoverRect.width / 2);
93
+ var spaceBelow = viewportHeight - rect.bottom;
94
+ var spaceAbove = rect.top;
95
+ var openAbove = spaceBelow < popoverRect.height + gap + padding && spaceAbove > spaceBelow;
96
+
97
+ var top;
98
+ if (openAbove) {
99
+ top = rect.top + scrollY - popoverRect.height - gap;
100
+ annotationPopover.classList.add('is-above');
101
+ } else {
102
+ top = rect.bottom + scrollY + gap;
103
+ annotationPopover.classList.remove('is-above');
104
+ }
105
+
106
+ if (left < padding) {
107
+ left = padding;
108
+ } else if (left + popoverRect.width > viewportWidth - padding) {
109
+ left = viewportWidth - popoverRect.width - padding;
110
+ }
111
+
112
+ var arrowLeft = rect.left + scrollX + (rect.width / 2) - left;
113
+ annotationPopover.style.setProperty('--arrow-left', Math.max(12, Math.min(arrowLeft, popoverRect.width - 12)) + 'px');
114
+ annotationPopover.style.left = left + 'px';
115
+ annotationPopover.style.top = top + 'px';
116
+ annotationPopover.style.visibility = 'visible';
117
+ }
118
+
119
+ function hideAnnotationPopover() {
120
+ if (!annotationPopover) return;
121
+ if (activeAnnotationButton) {
122
+ setAnnotationIcon(activeAnnotationButton, 'plus-circle', true);
123
+ activeAnnotationButton.classList.remove('is-active');
124
+ }
125
+ annotationPopover.classList.remove('is-visible');
126
+ activeAnnotationButton = null;
127
+ }
128
+
129
+ if (document.readyState === 'loading') {
130
+ document.addEventListener('DOMContentLoaded', function() { initCodeAnnotations(); });
131
+ } else {
132
+ initCodeAnnotations();
133
+ }
134
+
135
+ window.docyard = window.docyard || {};
136
+ window.docyard.initCodeAnnotations = initCodeAnnotations;
@@ -59,6 +59,7 @@
59
59
  if (docyard.initAbbreviations) docyard.initAbbreviations(container);
60
60
  if (docyard.initLightbox) docyard.initLightbox(container);
61
61
  if (docyard.initTooltips) docyard.initTooltips(container);
62
+ if (docyard.initCodeAnnotations) docyard.initCodeAnnotations(container);
62
63
  }
63
64
 
64
65
  function reloadContent() {
@@ -1,4 +1,4 @@
1
- <% has_diff = @diff_lines&.any? %><% has_focus = @focus_lines&.any? %><% has_error = @error_lines&.any? %><% has_warning = @warning_lines&.any? %><% has_title = !@title.nil? && !@title.empty? %><div class="docyard-code-block<%= ' docyard-code-block--line-numbers' if @show_line_numbers %><%= ' docyard-code-block--highlighted' if @highlights&.any? %><%= ' docyard-code-block--diff' if has_diff %><%= ' docyard-code-block--has-focus' if has_focus %><%= ' docyard-code-block--has-error' if has_error %><%= ' docyard-code-block--has-warning' if has_warning %><%= ' docyard-code-block--titled' if has_title %>" data-pagefind-ignore>
1
+ <% has_diff = @diff_lines&.any? %><% has_focus = @focus_lines&.any? %><% has_error = @error_lines&.any? %><% has_warning = @warning_lines&.any? %><% has_annotations = @annotation_markers&.any? %><% has_title = !@title.nil? && !@title.empty? %><div class="docyard-code-block<%= ' docyard-code-block--line-numbers' if @show_line_numbers %><%= ' docyard-code-block--highlighted' if @highlights&.any? %><%= ' docyard-code-block--diff' if has_diff %><%= ' docyard-code-block--has-focus' if has_focus %><%= ' docyard-code-block--has-error' if has_error %><%= ' docyard-code-block--has-warning' if has_warning %><%= ' docyard-code-block--has-annotations' if has_annotations %><%= ' docyard-code-block--titled' if has_title %>" data-pagefind-ignore>
2
2
  <% if has_title %>
3
3
  <div class="docyard-code-block__header">
4
4
  <% if @icon %>
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docyard
4
- VERSION = "1.3.0"
4
+ VERSION = "1.4.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: docyard
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sanif Himani
@@ -83,30 +83,42 @@ dependencies:
83
83
  name: parallel
84
84
  requirement: !ruby/object:Gem::Requirement
85
85
  requirements:
86
- - - "~>"
86
+ - - ">="
87
87
  - !ruby/object:Gem::Version
88
88
  version: '1.26'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '3.0'
89
92
  type: :runtime
90
93
  prerelease: false
91
94
  version_requirements: !ruby/object:Gem::Requirement
92
95
  requirements:
93
- - - "~>"
96
+ - - ">="
94
97
  - !ruby/object:Gem::Version
95
98
  version: '1.26'
99
+ - - "<"
100
+ - !ruby/object:Gem::Version
101
+ version: '3.0'
96
102
  - !ruby/object:Gem::Dependency
97
103
  name: puma
98
104
  requirement: !ruby/object:Gem::Requirement
99
105
  requirements:
100
- - - "~>"
106
+ - - ">="
101
107
  - !ruby/object:Gem::Version
102
- version: '7.0'
108
+ version: '7'
109
+ - - "<"
110
+ - !ruby/object:Gem::Version
111
+ version: '9'
103
112
  type: :runtime
104
113
  prerelease: false
105
114
  version_requirements: !ruby/object:Gem::Requirement
106
115
  requirements:
107
- - - "~>"
116
+ - - ">="
108
117
  - !ruby/object:Gem::Version
109
- version: '7.0'
118
+ version: '7'
119
+ - - "<"
120
+ - !ruby/object:Gem::Version
121
+ version: '9'
110
122
  - !ruby/object:Gem::Dependency
111
123
  name: rack
112
124
  requirement: !ruby/object:Gem::Requirement
@@ -125,16 +137,22 @@ dependencies:
125
137
  name: rouge
126
138
  requirement: !ruby/object:Gem::Requirement
127
139
  requirements:
128
- - - "~>"
140
+ - - ">="
129
141
  - !ruby/object:Gem::Version
130
- version: '4.0'
142
+ version: '4'
143
+ - - "<"
144
+ - !ruby/object:Gem::Version
145
+ version: '6'
131
146
  type: :runtime
132
147
  prerelease: false
133
148
  version_requirements: !ruby/object:Gem::Requirement
134
149
  requirements:
135
- - - "~>"
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '4'
153
+ - - "<"
136
154
  - !ruby/object:Gem::Version
137
- version: '4.0'
155
+ version: '6'
138
156
  - !ruby/object:Gem::Dependency
139
157
  name: terser
140
158
  requirement: !ruby/object:Gem::Requirement
@@ -199,6 +217,7 @@ files:
199
217
  - lib/docyard/components/processors/badge_processor.rb
200
218
  - lib/docyard/components/processors/callout_processor.rb
201
219
  - lib/docyard/components/processors/cards_processor.rb
220
+ - lib/docyard/components/processors/code_block_annotation_preprocessor.rb
202
221
  - lib/docyard/components/processors/code_block_diff_preprocessor.rb
203
222
  - lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb
204
223
  - lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb
@@ -218,8 +237,10 @@ files:
218
237
  - lib/docyard/components/processors/table_wrapper_processor.rb
219
238
  - lib/docyard/components/processors/tabs_processor.rb
220
239
  - lib/docyard/components/processors/tooltip_processor.rb
240
+ - lib/docyard/components/processors/variables_processor.rb
221
241
  - lib/docyard/components/processors/video_embed_processor.rb
222
242
  - lib/docyard/components/registry.rb
243
+ - lib/docyard/components/support/code_block/annotation_list_parser.rb
223
244
  - lib/docyard/components/support/code_block/feature_extractor.rb
224
245
  - lib/docyard/components/support/code_block/icon_detector.rb
225
246
  - lib/docyard/components/support/code_block/line_number_resolver.rb
@@ -330,6 +351,7 @@ files:
330
351
  - lib/docyard/templates/assets/css/components/breadcrumbs.css
331
352
  - lib/docyard/templates/assets/css/components/callout.css
332
353
  - lib/docyard/templates/assets/css/components/cards.css
354
+ - lib/docyard/templates/assets/css/components/code-annotation.css
333
355
  - lib/docyard/templates/assets/css/components/code-block.css
334
356
  - lib/docyard/templates/assets/css/components/code-group.css
335
357
  - lib/docyard/templates/assets/css/components/feedback.css
@@ -363,6 +385,7 @@ files:
363
385
  - lib/docyard/templates/assets/fonts/Inter-Variable.woff2
364
386
  - lib/docyard/templates/assets/js/components/abbreviation.js
365
387
  - lib/docyard/templates/assets/js/components/banner.js
388
+ - lib/docyard/templates/assets/js/components/code-annotation.js
366
389
  - lib/docyard/templates/assets/js/components/code-block.js
367
390
  - lib/docyard/templates/assets/js/components/code-group.js
368
391
  - lib/docyard/templates/assets/js/components/copy-page.js