docyard 1.2.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 +4 -4
- data/CHANGELOG.md +24 -1
- data/lib/docyard/cli.rb +17 -0
- data/lib/docyard/components/aliases.rb +2 -0
- data/lib/docyard/components/processors/code_block_annotation_preprocessor.rb +149 -0
- data/lib/docyard/components/processors/code_block_processor.rb +21 -11
- data/lib/docyard/components/processors/variables_processor.rb +56 -0
- data/lib/docyard/components/support/code_block/annotation_list_parser.rb +84 -0
- data/lib/docyard/components/support/code_block/line_wrapper.rb +25 -1
- data/lib/docyard/components/support/code_block/patterns.rb +11 -0
- data/lib/docyard/config/schema/definition.rb +2 -1
- data/lib/docyard/config.rb +3 -1
- data/lib/docyard/deploy/adapters/base.rb +56 -0
- data/lib/docyard/deploy/adapters/cloudflare.rb +38 -0
- data/lib/docyard/deploy/adapters/github_pages.rb +60 -0
- data/lib/docyard/deploy/adapters/netlify.rb +37 -0
- data/lib/docyard/deploy/adapters/vercel.rb +36 -0
- data/lib/docyard/deploy/deployer.rb +95 -0
- data/lib/docyard/deploy/platform_detector.rb +32 -0
- data/lib/docyard/errors.rb +2 -0
- data/lib/docyard/rendering/markdown.rb +2 -0
- data/lib/docyard/templates/assets/css/components/code-annotation.css +265 -0
- data/lib/docyard/templates/assets/css/variables.css +2 -0
- data/lib/docyard/templates/assets/js/components/code-annotation.js +136 -0
- data/lib/docyard/templates/assets/js/hot-reload.js +1 -0
- data/lib/docyard/templates/partials/_code_block.html.erb +1 -1
- data/lib/docyard/version.rb +1 -1
- metadata +13 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c329f7664eff5bca0fded133cdc29fffa5eea23f81a0183605ea1f038adfa819
|
|
4
|
+
data.tar.gz: 5f4c135cd6a7dc25140d8111940d6a2fda14c4c9b142adb3d0c8b8a2c91a8f30
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bb2ccd83fef6197381df0dee879b71005fa6745842c242bd2fe4e2622d172356f0919b165c63791d6999f04f1cf8715f4dc5338d6050203f5133345ad52fcd9a
|
|
7
|
+
data.tar.gz: 7a6ea46ba7dd06050e4157c018d158c904a9fbb86dcce352446a4b7d8c99199b641cd8094d5a5eb4074e59716df2abd3e30925fc19c8d4232f365a11d20524a5
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,27 @@ 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
|
+
|
|
20
|
+
## [1.3.0] - 2026-02-17
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- **Deploy Command** - One-command deployment with `docyard deploy` supporting Vercel, Netlify, Cloudflare Pages, and GitHub Pages (#153)
|
|
24
|
+
- **Platform Auto-Detection** - Automatically detects deployment platform from project config files (e.g. `vercel.json`, `netlify.toml`)
|
|
25
|
+
|
|
26
|
+
### Documentation
|
|
27
|
+
- Added Deploy Command page with per-platform setup instructions
|
|
28
|
+
- Updated CLI reference with `docyard deploy` options
|
|
29
|
+
- Cross-linked existing GitHub Pages, Vercel, and Netlify docs to deploy command
|
|
30
|
+
|
|
10
31
|
## [1.2.0] - 2026-02-03
|
|
11
32
|
|
|
12
33
|
### Added
|
|
@@ -264,7 +285,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
264
285
|
- Initial gem structure
|
|
265
286
|
- Project scaffolding
|
|
266
287
|
|
|
267
|
-
[Unreleased]: https://github.com/sanifhimani/docyard/compare/v1.
|
|
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
|
|
290
|
+
[1.3.0]: https://github.com/sanifhimani/docyard/compare/v1.2.0...v1.3.0
|
|
268
291
|
[1.2.0]: https://github.com/sanifhimani/docyard/compare/v1.1.0...v1.2.0
|
|
269
292
|
[1.1.0]: https://github.com/sanifhimani/docyard/compare/v1.0.2...v1.1.0
|
|
270
293
|
[1.0.2]: https://github.com/sanifhimani/docyard/compare/v1.0.1...v1.0.2
|
data/lib/docyard/cli.rb
CHANGED
|
@@ -80,6 +80,23 @@ module Docyard
|
|
|
80
80
|
exit(doctor.run)
|
|
81
81
|
end
|
|
82
82
|
|
|
83
|
+
desc "deploy", "Deploy the built site to a hosting platform"
|
|
84
|
+
method_option :to, type: :string, desc: "Target platform (vercel, netlify, cloudflare, github-pages)"
|
|
85
|
+
method_option :prod, type: :boolean, default: true, desc: "Deploy to production"
|
|
86
|
+
method_option :skip_build, type: :boolean, default: false, desc: "Skip building before deploy"
|
|
87
|
+
def deploy
|
|
88
|
+
apply_global_options
|
|
89
|
+
require_relative "deploy/deployer"
|
|
90
|
+
deployer = Deploy::Deployer.new(
|
|
91
|
+
to: options[:to],
|
|
92
|
+
production: options[:prod],
|
|
93
|
+
skip_build: options[:skip_build]
|
|
94
|
+
)
|
|
95
|
+
exit(1) unless deployer.deploy
|
|
96
|
+
rescue ConfigError => e
|
|
97
|
+
print_config_error(e)
|
|
98
|
+
end
|
|
99
|
+
|
|
83
100
|
desc "customize", "Generate theme customization files"
|
|
84
101
|
method_option :minimal, type: :boolean, default: false, aliases: "-m",
|
|
85
102
|
desc: "Generate minimal files without comments"
|
|
@@ -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
|
-
@
|
|
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
|
-
|
|
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
|
-
|
|
109
|
-
|
|
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
|
-
|
|
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("&", "&").gsub("<", "<").gsub(">", ">").gsub('"', """)
|
|
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
|
data/lib/docyard/config.rb
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Deploy
|
|
7
|
+
module Adapters
|
|
8
|
+
class Base
|
|
9
|
+
attr_reader :output_dir, :production, :config
|
|
10
|
+
|
|
11
|
+
def initialize(output_dir:, production:, config:)
|
|
12
|
+
@output_dir = output_dir
|
|
13
|
+
@production = production
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def deploy
|
|
18
|
+
check_cli_installed!
|
|
19
|
+
run_deploy
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def platform_name
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def cli_name
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def cli_install_hint
|
|
33
|
+
raise NotImplementedError
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def run_deploy
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def check_cli_installed!
|
|
41
|
+
_, _, status = Open3.capture3("which", cli_name)
|
|
42
|
+
return if status.success?
|
|
43
|
+
|
|
44
|
+
raise DeployError, "'#{cli_name}' CLI not found. Install it with: #{cli_install_hint}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def execute_command(*)
|
|
48
|
+
stdout, stderr, status = Open3.capture3(*)
|
|
49
|
+
return stdout if status.success?
|
|
50
|
+
|
|
51
|
+
raise DeployError, "Deploy command failed: #{stderr.strip.empty? ? stdout.strip : stderr.strip}"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base"
|
|
4
|
+
|
|
5
|
+
module Docyard
|
|
6
|
+
module Deploy
|
|
7
|
+
module Adapters
|
|
8
|
+
class Cloudflare < Base
|
|
9
|
+
def platform_name
|
|
10
|
+
"Cloudflare Pages"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
def cli_name
|
|
16
|
+
"wrangler"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cli_install_hint
|
|
20
|
+
"npm i -g wrangler"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run_deploy
|
|
24
|
+
output = execute_command("wrangler", "pages", "deploy", output_dir, "--project-name=#{project_name}")
|
|
25
|
+
extract_url(output)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def project_name
|
|
29
|
+
config.title.downcase.gsub(/[^a-z0-9]+/, "-").gsub(/\A-|-\z/, "")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def extract_url(output)
|
|
33
|
+
output.match(%r{https://\S+\.pages\.dev\S*})&.to_s
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|