docyard 1.3.0 → 1.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4892fa130361d02f347660d81932135f00c71d339c28b292e7bfecbe46ededa0
4
- data.tar.gz: 4697d95492f675b935db0dad134c11eb0d57f7aca611dd25c3e4e22bded9f031
3
+ metadata.gz: c329f7664eff5bca0fded133cdc29fffa5eea23f81a0183605ea1f038adfa819
4
+ data.tar.gz: 5f4c135cd6a7dc25140d8111940d6a2fda14c4c9b142adb3d0c8b8a2c91a8f30
5
5
  SHA512:
6
- metadata.gz: 342e2e7f16c32da9da273b6325532ad8ee842d58304faf309a0e634be5f7c2d3a6a8c5eb0176b09e0b518a67150c25223b5ad3be9c67da90e974e4be9871e28e
7
- data.tar.gz: 2f1b65cde20d044348908884fbfd0a185d47d6ef73d9fdd516e5f11792262700550bb70d6ec7523dc910dcaed864d2df51bf6f54480997092c63dfe926b6629d
6
+ metadata.gz: bb2ccd83fef6197381df0dee879b71005fa6745842c242bd2fe4e2622d172356f0919b165c63791d6999f04f1cf8715f4dc5338d6050203f5133345ad52fcd9a
7
+ data.tar.gz: 7a6ea46ba7dd06050e4157c018d158c904a9fbb86dcce352446a4b7d8c99199b641cd8094d5a5eb4074e59716df2abd3e30925fc19c8d4232f365a11d20524a5
data/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.4.0] - 2026-02-22
11
+
12
+ ### Added
13
+ - **Code Annotations** - Clickable inline markers in code blocks with popover explanations using `// (1)` syntax and a matching ordered list (#156)
14
+ - **Variables** - Template variables with `{{ variable }}` syntax, defined in `docyard.yml` under `variables:` (#155)
15
+
16
+ ### Documentation
17
+ - Added Code Annotations section to Code Blocks page
18
+ - Added Variables page with usage examples and configuration reference
19
+
10
20
  ## [1.3.0] - 2026-02-17
11
21
 
12
22
  ### Added
@@ -275,7 +285,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
275
285
  - Initial gem structure
276
286
  - Project scaffolding
277
287
 
278
- [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.3.0...HEAD
288
+ [Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.4.0...HEAD
289
+ [1.4.0]: https://github.com/sanifhimani/docyard/compare/v1.3.0...v1.4.0
279
290
  [1.3.0]: https://github.com/sanifhimani/docyard/compare/v1.2.0...v1.3.0
280
291
  [1.2.0]: https://github.com/sanifhimani/docyard/compare/v1.1.0...v1.2.0
281
292
  [1.1.0]: https://github.com/sanifhimani/docyard/compare/v1.0.2...v1.1.0
@@ -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
@@ -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.0"
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.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sanif Himani
@@ -199,6 +199,7 @@ files:
199
199
  - lib/docyard/components/processors/badge_processor.rb
200
200
  - lib/docyard/components/processors/callout_processor.rb
201
201
  - lib/docyard/components/processors/cards_processor.rb
202
+ - lib/docyard/components/processors/code_block_annotation_preprocessor.rb
202
203
  - lib/docyard/components/processors/code_block_diff_preprocessor.rb
203
204
  - lib/docyard/components/processors/code_block_extended_fence_postprocessor.rb
204
205
  - lib/docyard/components/processors/code_block_extended_fence_preprocessor.rb
@@ -218,8 +219,10 @@ files:
218
219
  - lib/docyard/components/processors/table_wrapper_processor.rb
219
220
  - lib/docyard/components/processors/tabs_processor.rb
220
221
  - lib/docyard/components/processors/tooltip_processor.rb
222
+ - lib/docyard/components/processors/variables_processor.rb
221
223
  - lib/docyard/components/processors/video_embed_processor.rb
222
224
  - lib/docyard/components/registry.rb
225
+ - lib/docyard/components/support/code_block/annotation_list_parser.rb
223
226
  - lib/docyard/components/support/code_block/feature_extractor.rb
224
227
  - lib/docyard/components/support/code_block/icon_detector.rb
225
228
  - lib/docyard/components/support/code_block/line_number_resolver.rb
@@ -330,6 +333,7 @@ files:
330
333
  - lib/docyard/templates/assets/css/components/breadcrumbs.css
331
334
  - lib/docyard/templates/assets/css/components/callout.css
332
335
  - lib/docyard/templates/assets/css/components/cards.css
336
+ - lib/docyard/templates/assets/css/components/code-annotation.css
333
337
  - lib/docyard/templates/assets/css/components/code-block.css
334
338
  - lib/docyard/templates/assets/css/components/code-group.css
335
339
  - lib/docyard/templates/assets/css/components/feedback.css
@@ -363,6 +367,7 @@ files:
363
367
  - lib/docyard/templates/assets/fonts/Inter-Variable.woff2
364
368
  - lib/docyard/templates/assets/js/components/abbreviation.js
365
369
  - lib/docyard/templates/assets/js/components/banner.js
370
+ - lib/docyard/templates/assets/js/components/code-annotation.js
366
371
  - lib/docyard/templates/assets/js/components/code-block.js
367
372
  - lib/docyard/templates/assets/js/components/code-group.js
368
373
  - lib/docyard/templates/assets/js/components/copy-page.js