charming 0.1.3 → 0.1.4
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/lib/charming/application.rb +19 -2
- data/lib/charming/cli.rb +3 -3
- data/lib/charming/controller/component_dispatching.rb +47 -3
- data/lib/charming/controller/focus.rb +123 -0
- data/lib/charming/controller/focus_management.rb +1 -1
- data/lib/charming/controller/rendering.rb +4 -15
- data/lib/charming/controller/session_state.rb +11 -0
- data/lib/charming/controller.rb +11 -2
- data/lib/charming/database/commands.rb +106 -0
- data/lib/charming/generators/database_installer.rb +154 -0
- data/lib/charming/generators/model_generator.rb +2 -10
- data/lib/charming/generators/name.rb +1 -1
- data/lib/charming/generators/view_generator.rb +1 -1
- data/lib/charming/presentation/components/form/field.rb +1 -1
- data/lib/charming/presentation/components/markdown.rb +7 -7
- data/lib/charming/presentation/layout/pane.rb +7 -0
- data/lib/charming/presentation/layout/rect.rb +5 -0
- data/lib/charming/presentation/layout/screen_layout.rb +7 -0
- data/lib/charming/presentation/layout/split.rb +7 -0
- data/lib/charming/presentation/markdown/render_context.rb +28 -10
- data/lib/charming/presentation/markdown/renderer.rb +264 -39
- data/lib/charming/presentation/markdown/style_config.rb +215 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +3 -2
- data/lib/charming/presentation/markdown.rb +2 -2
- data/lib/charming/presentation/view.rb +7 -0
- data/lib/charming/router.rb +3 -8
- data/lib/charming/runtime.rb +2 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +2 -2
- metadata +42 -9
- data/lib/charming/database_commands.rb +0 -103
- data/lib/charming/database_installer.rb +0 -152
- data/lib/charming/focus.rb +0 -121
- data/lib/charming/presentation/markdown/block_renderers.rb +0 -118
- data/lib/charming/presentation/markdown/inline_renderers.rb +0 -66
|
@@ -2,17 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Components
|
|
5
|
-
# Markdown renders
|
|
6
|
-
# `Charming::Markdown::Renderer`; set *syntax_highlighting* to false to disable
|
|
7
|
-
# Rouge-backed code block highlighting.
|
|
5
|
+
# Markdown renders CommonMark/GFM source as ANSI-styled terminal text.
|
|
8
6
|
class Markdown < Component
|
|
9
|
-
|
|
10
|
-
# *syntax_highlighting* enables Rouge for code blocks (defaults to true).
|
|
11
|
-
def initialize(content:, width: nil, theme: nil, syntax_highlighting: true)
|
|
7
|
+
def initialize(content:, width: nil, theme: nil, syntax_highlighting: true, style: :dark, base_url: nil)
|
|
12
8
|
super(theme: theme)
|
|
13
9
|
@content = content
|
|
14
10
|
@width = width
|
|
15
11
|
@syntax_highlighting = syntax_highlighting
|
|
12
|
+
@style = style
|
|
13
|
+
@base_url = base_url
|
|
16
14
|
end
|
|
17
15
|
|
|
18
16
|
# Renders the Markdown body to a styled, terminal-safe string.
|
|
@@ -21,7 +19,9 @@ module Charming
|
|
|
21
19
|
content: @content,
|
|
22
20
|
width: @width,
|
|
23
21
|
theme: theme,
|
|
24
|
-
syntax_highlighting: @syntax_highlighting
|
|
22
|
+
syntax_highlighting: @syntax_highlighting,
|
|
23
|
+
style: @style,
|
|
24
|
+
base_url: @base_url
|
|
25
25
|
).render
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -44,6 +44,13 @@ module Charming
|
|
|
44
44
|
(focus && name) ? [name] : []
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# Returns the mouse target represented by this pane, if it has a name.
|
|
48
|
+
def mouse_targets(rect)
|
|
49
|
+
return [] unless name
|
|
50
|
+
|
|
51
|
+
[{name: name, rect: rect, inner_rect: inner_rect(rect)}]
|
|
52
|
+
end
|
|
53
|
+
|
|
47
54
|
# Renders the pane into *rect*, applying the configured style, border, and padding
|
|
48
55
|
# around the evaluated content.
|
|
49
56
|
def render(rect)
|
|
@@ -6,6 +6,11 @@ module Charming
|
|
|
6
6
|
# (width, height). Layout operations produce new Rect instances rather than mutating
|
|
7
7
|
# existing ones.
|
|
8
8
|
Rect = Data.define(:x, :y, :width, :height) do
|
|
9
|
+
# Returns true when the zero-based cell coordinate falls within this rectangle.
|
|
10
|
+
def cover?(point_x, point_y)
|
|
11
|
+
point_x >= x && point_x < x + width && point_y >= y && point_y < y + height
|
|
12
|
+
end
|
|
13
|
+
|
|
9
14
|
# Returns a new Rect inset by *top*/*right*/*bottom*/*left* cells. The result is
|
|
10
15
|
# clamped to a minimum width/height of 0.
|
|
11
16
|
def inset(top: 0, right: 0, bottom: 0, left: 0)
|
|
@@ -32,6 +32,13 @@ module Charming
|
|
|
32
32
|
child ? child.focusable_names : []
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Returns all named pane mouse targets for the full screen layout.
|
|
36
|
+
def mouse_targets
|
|
37
|
+
return [] unless child
|
|
38
|
+
|
|
39
|
+
child.mouse_targets(Rect.new(x: 0, y: 0, width: screen.width, height: screen.height))
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
# Renders the child into the full-screen rect, then overlays each registered overlay
|
|
36
43
|
# on top in order.
|
|
37
44
|
def render
|
|
@@ -32,6 +32,13 @@ module Charming
|
|
|
32
32
|
children.flat_map(&:focusable_names)
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Returns all named pane mouse targets in this split, preserving declaration order.
|
|
36
|
+
def mouse_targets(rect)
|
|
37
|
+
child_rects(rect).zip(children).flat_map do |child_rect, child|
|
|
38
|
+
child.respond_to?(:mouse_targets) ? child.mouse_targets(child_rect) : []
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
# Renders each child into its own sub-rect, then overlays them on a blank canvas
|
|
36
43
|
# of the parent's dimensions.
|
|
37
44
|
def render(rect)
|
|
@@ -2,18 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
4
|
module Markdown
|
|
5
|
-
# RenderContext carries
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
# RenderContext carries render-time state that needs to flow down the Markdown AST.
|
|
6
|
+
RenderContext = Data.define(:width, :list_depth, :style, :current_style, :base_url, :source_lines) do
|
|
7
|
+
def self.from(width:, style:, base_url: nil, source_lines: [], list_depth: 0, current_style: nil)
|
|
8
|
+
new(
|
|
9
|
+
width: width,
|
|
10
|
+
list_depth: list_depth,
|
|
11
|
+
style: style,
|
|
12
|
+
current_style: current_style || style[:document],
|
|
13
|
+
base_url: base_url,
|
|
14
|
+
source_lines: source_lines
|
|
15
|
+
)
|
|
11
16
|
end
|
|
12
17
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
def with(width: self.width, list_depth: self.list_depth, current_style: self.current_style)
|
|
19
|
+
self.class.new(
|
|
20
|
+
width: width,
|
|
21
|
+
list_depth: list_depth,
|
|
22
|
+
style: style,
|
|
23
|
+
current_style: current_style,
|
|
24
|
+
base_url: base_url,
|
|
25
|
+
source_lines: source_lines
|
|
26
|
+
)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def nested_list(width: self.width)
|
|
30
|
+
with(width: width, list_depth: list_depth + 1)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def inherit(style_name)
|
|
34
|
+
current_style.inherit_visual(style[style_name])
|
|
17
35
|
end
|
|
18
36
|
end
|
|
19
37
|
end
|
|
@@ -1,72 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "commonmarker"
|
|
4
|
+
require "uri"
|
|
4
5
|
|
|
5
6
|
module Charming
|
|
6
7
|
module Markdown
|
|
7
|
-
# Renderer
|
|
8
|
-
# Kramdown, then walks the document's block and inline trees to produce styled
|
|
9
|
-
# terminal output. Code blocks are highlighted via Rouge when `syntax_highlighting`
|
|
10
|
-
# is enabled.
|
|
8
|
+
# Renderer parses CommonMark/GFM with Commonmarker and renders it as ANSI text.
|
|
11
9
|
class Renderer
|
|
12
|
-
# Wrap width used by `render_rule` when no width is otherwise specified.
|
|
13
10
|
DEFAULT_RULE_WIDTH = 40
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
attr_reader :content, :width, :theme, :syntax_highlighting
|
|
12
|
+
attr_reader :content, :width, :theme, :syntax_highlighting, :style, :base_url
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
# many display columns. *theme* is the Charming theme used to style blocks/inlines.
|
|
20
|
-
# *syntax_highlighting* enables Rouge-backed code block highlighting (default true).
|
|
21
|
-
def initialize(content:, width: nil, theme: UI::Theme.default, syntax_highlighting: true)
|
|
14
|
+
def initialize(content:, width: nil, theme: UI::Theme.default, syntax_highlighting: true, style: :dark, base_url: nil)
|
|
22
15
|
@content = content
|
|
23
16
|
@width = width
|
|
24
17
|
@theme = theme || UI::Theme.default
|
|
25
18
|
@syntax_highlighting = syntax_highlighting
|
|
19
|
+
@style = StyleConfig.from(style)
|
|
20
|
+
@base_url = base_url
|
|
26
21
|
end
|
|
27
22
|
|
|
28
|
-
# Parses the content and returns the fully-rendered Markdown as a single string.
|
|
29
23
|
def render
|
|
30
|
-
|
|
31
|
-
|
|
24
|
+
context = RenderContext.from(
|
|
25
|
+
width: width,
|
|
26
|
+
style: style,
|
|
27
|
+
base_url: base_url,
|
|
28
|
+
source_lines: content.to_s.lines(chomp: true)
|
|
29
|
+
)
|
|
30
|
+
render_document(parse_document, context: context)
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
# *list_depth* is forwarded to the render context for list indentation. *width*
|
|
36
|
-
# defaults to the renderer's configured width.
|
|
37
|
-
def render_blocks(elements, list_depth: 0, width: @width)
|
|
38
|
-
context = RenderContext.from(width: width, list_depth: list_depth)
|
|
33
|
+
def render_blocks(elements, context:)
|
|
39
34
|
elements.filter_map do |element|
|
|
40
|
-
rendered =
|
|
35
|
+
rendered = render_block(element, context: context)
|
|
41
36
|
rendered unless rendered.to_s.empty?
|
|
42
37
|
end.join("\n\n")
|
|
43
38
|
end
|
|
44
39
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def render_inlines(elements, width: @width)
|
|
48
|
-
context = RenderContext.from(width: width)
|
|
49
|
-
elements.map { |element| inline_renderer.render(element, context: context) }.join
|
|
40
|
+
def render_inlines(elements, context:)
|
|
41
|
+
elements.map { |element| render_inline(element, context: context) }.join
|
|
50
42
|
end
|
|
51
43
|
|
|
52
|
-
# Word-wraps *value* to *width* display columns (when *width* is given), preserving
|
|
53
|
-
# any ANSI styling on each line. Returns *value* unchanged when *width* is nil.
|
|
54
44
|
def wrap(value, width:)
|
|
55
45
|
return value unless width
|
|
56
46
|
|
|
57
47
|
value.to_s.lines(chomp: true).map { |line| wrap_line(line, width) }.join("\n")
|
|
58
48
|
end
|
|
59
49
|
|
|
60
|
-
# Returns the theme's style for *name* if the theme defines it, otherwise returns
|
|
61
|
-
# *fallback*. Lets views override markdown-specific theme tokens.
|
|
62
50
|
def style_for(name, fallback:)
|
|
63
51
|
return theme.public_send(name) if theme.respond_to?(name)
|
|
64
52
|
|
|
65
53
|
fallback
|
|
66
54
|
end
|
|
67
55
|
|
|
68
|
-
# Returns the theme's style for *name*, building a one-token default theme when
|
|
69
|
-
# the active theme doesn't define it. Used as a final fallback for markdown styling.
|
|
70
56
|
def theme_style(name)
|
|
71
57
|
return theme.public_send(name) if theme.respond_to?(name)
|
|
72
58
|
|
|
@@ -75,17 +61,256 @@ module Charming
|
|
|
75
61
|
|
|
76
62
|
private
|
|
77
63
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
64
|
+
def parse_document
|
|
65
|
+
Commonmarker.parse(
|
|
66
|
+
content.to_s,
|
|
67
|
+
options: {
|
|
68
|
+
extension: {
|
|
69
|
+
autolink: true,
|
|
70
|
+
description_lists: true,
|
|
71
|
+
footnotes: true,
|
|
72
|
+
strikethrough: true,
|
|
73
|
+
table: true,
|
|
74
|
+
tasklist: true
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
)
|
|
81
78
|
end
|
|
82
79
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
def render_document(node, context:)
|
|
81
|
+
document_style = context.style[:document]
|
|
82
|
+
body = render_blocks(children_of(node), context: context.with(current_style: document_style))
|
|
83
|
+
document_style.render(document_style.apply_block_layout(body))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def render_block(node, context:)
|
|
87
|
+
case node.type
|
|
88
|
+
when :paragraph
|
|
89
|
+
render_paragraph(node, context: context)
|
|
90
|
+
when :heading
|
|
91
|
+
render_heading(node, context: context)
|
|
92
|
+
when :block_quote
|
|
93
|
+
render_block_quote(node, context: context)
|
|
94
|
+
when :list
|
|
95
|
+
render_list(node, context: context)
|
|
96
|
+
when :code_block
|
|
97
|
+
render_code_block(node, context: context)
|
|
98
|
+
when :thematic_break
|
|
99
|
+
render_rule(context: context)
|
|
100
|
+
when :table
|
|
101
|
+
render_table(node, context: context)
|
|
102
|
+
when :html_block
|
|
103
|
+
render_html_block(node, context: context)
|
|
104
|
+
else
|
|
105
|
+
render_blocks(children_of(node), context: context)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def render_inline(node, context:)
|
|
110
|
+
case node.type
|
|
111
|
+
when :text
|
|
112
|
+
context.current_style.inherit_visual(context.style[:text]).render(node.string_content)
|
|
113
|
+
when :softbreak
|
|
114
|
+
" "
|
|
115
|
+
when :linebreak
|
|
116
|
+
"\n"
|
|
117
|
+
when :code
|
|
118
|
+
context.inherit(:code).render(node.string_content)
|
|
119
|
+
when :emph
|
|
120
|
+
render_styled_inline(node, :emph, context: context)
|
|
121
|
+
when :strong
|
|
122
|
+
render_styled_inline(node, :strong, context: context)
|
|
123
|
+
when :strikethrough
|
|
124
|
+
render_styled_inline(node, :strikethrough, context: context)
|
|
125
|
+
when :link
|
|
126
|
+
render_link(node, context: context)
|
|
127
|
+
when :image
|
|
128
|
+
render_image(node, context: context)
|
|
129
|
+
when :html_inline
|
|
130
|
+
""
|
|
131
|
+
else
|
|
132
|
+
render_inlines(children_of(node), context: context)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def render_paragraph(node, context:)
|
|
137
|
+
paragraph_style = context.current_style.inherit_visual(context.style[:paragraph])
|
|
138
|
+
body = render_inlines(children_of(node), context: context.with(current_style: paragraph_style))
|
|
139
|
+
render_block_with_style(paragraph_style, wrap(body, width: context.width))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_heading(node, context:)
|
|
143
|
+
heading_style = context.current_style.inherit_visual(context.style.heading(node.header_level))
|
|
144
|
+
body = render_inlines(children_of(node), context: context.with(current_style: heading_style))
|
|
145
|
+
render_block_with_style(heading_style, wrap(body, width: context.width))
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def render_block_quote(node, context:)
|
|
149
|
+
quote_style = context.current_style.inherit_visual(context.style[:block_quote])
|
|
150
|
+
quote_width = context.width ? [context.width - quote_indent_width(quote_style), 1].max : nil
|
|
151
|
+
body = render_blocks(children_of(node), context: context.with(width: quote_width, current_style: quote_style))
|
|
152
|
+
render_block_with_style(quote_style, body)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def render_list(node, context:)
|
|
156
|
+
list_style = context.current_style.inherit_visual(context.style[:list])
|
|
157
|
+
children_of(node).each_with_index.map do |item, index|
|
|
158
|
+
render_list_item(item, index: index, ordered: node.list_type == :ordered, list: node, context: context.with(current_style: list_style))
|
|
159
|
+
end.join("\n")
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def render_list_item(node, index:, ordered:, list:, context:)
|
|
163
|
+
marker_style = context.current_style.inherit_visual(context.style[ordered ? :enumeration : :item])
|
|
164
|
+
marker = if node.type == :taskitem
|
|
165
|
+
task_marker(node, context: context)
|
|
166
|
+
elsif ordered
|
|
167
|
+
"#{list.list_start.to_i + index}. "
|
|
168
|
+
else
|
|
169
|
+
marker_style.block_prefix.empty? ? "- " : marker_style.block_prefix
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
indent = " " * (context.style[:list].level_indent || 2) * context.list_depth
|
|
173
|
+
first_prefix = "#{indent}#{marker}"
|
|
174
|
+
rest_prefix = "#{indent}#{" " * UI::Width.measure(marker)}"
|
|
175
|
+
item_width = context.width ? [context.width - UI::Width.measure(first_prefix), 1].max : nil
|
|
176
|
+
body = render_blocks(children_of(node), context: context.nested_list(width: item_width))
|
|
177
|
+
|
|
178
|
+
body.lines(chomp: true).each_with_index.map do |line, line_index|
|
|
179
|
+
"#{line_index.zero? ? first_prefix : rest_prefix}#{line}"
|
|
180
|
+
end.join("\n")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def task_marker(node, context:)
|
|
184
|
+
task_style = context.current_style.inherit_visual(context.style[:task])
|
|
185
|
+
checked_task?(node, context: context) ? (task_style.ticked || "[x] ") : (task_style.unticked || "[ ] ")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def checked_task?(node, context:)
|
|
189
|
+
line = context.source_lines[node.source_position[:start_line].to_i - 1].to_s
|
|
190
|
+
line.match?(/\[[xX]\]/)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def render_code_block(node, context:)
|
|
194
|
+
code_style = context.current_style.inherit_visual(context.style[:code_block])
|
|
195
|
+
code = node.string_content.to_s.chomp
|
|
196
|
+
rendered = if syntax_highlighting
|
|
197
|
+
SyntaxHighlighter.new(theme: theme, style: style).render(code, language: node.fence_info.to_s.split.first)
|
|
198
|
+
else
|
|
199
|
+
code_style.render(code)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
body = rendered.lines(chomp: true).map { |line| " #{line}" }.join("\n")
|
|
203
|
+
syntax_highlighting ? code_style.apply_block_layout(body) : render_block_with_style(code_style, body)
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def render_rule(context:)
|
|
207
|
+
rule_style = context.current_style.inherit_visual(context.style[:hr])
|
|
208
|
+
body = rule_style.format.empty? ? "-" * (context.width || DEFAULT_RULE_WIDTH) : rule_style.format
|
|
209
|
+
render_block_with_style(rule_style, body)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def render_table(node, context:)
|
|
213
|
+
table_style = context.current_style.inherit_visual(context.style[:table])
|
|
214
|
+
rows = children_of(node).map do |row|
|
|
215
|
+
children_of(row).map { |cell| render_inlines(children_of(cell), context: context.with(current_style: table_style)) }
|
|
216
|
+
end
|
|
217
|
+
return "" if rows.empty?
|
|
218
|
+
|
|
219
|
+
widths = table_widths(rows)
|
|
220
|
+
rendered_rows = rows.each_with_index.map do |row, index|
|
|
221
|
+
line = table_row(row, widths, table_style)
|
|
222
|
+
index.zero? ? [line, table_separator(widths, table_style)].join("\n") : line
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
render_block_with_style(table_style, rendered_rows.join("\n"))
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def render_html_block(_node, context:)
|
|
229
|
+
html_style = context.current_style.inherit_visual(context.style[:html_block])
|
|
230
|
+
return "" if html_style.format.empty?
|
|
231
|
+
|
|
232
|
+
render_block_with_style(html_style, html_style.format)
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def render_styled_inline(node, style_name, context:)
|
|
236
|
+
inline_style = context.inherit(style_name)
|
|
237
|
+
inline_style.render(render_inlines(children_of(node), context: context.with(current_style: inline_style)))
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def render_link(node, context:)
|
|
241
|
+
href = resolve_url(node.url.to_s, context: context)
|
|
242
|
+
text_style = context.inherit(:link_text)
|
|
243
|
+
link_style = context.inherit(:link)
|
|
244
|
+
label = render_inlines(children_of(node), context: context.with(current_style: text_style))
|
|
245
|
+
rendered = if href.empty? || UI::Width.strip_ansi(label) == href
|
|
246
|
+
label
|
|
247
|
+
else
|
|
248
|
+
"#{label} <#{href}>"
|
|
249
|
+
end
|
|
250
|
+
link_style.render(rendered)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def render_image(node, context:)
|
|
254
|
+
href = resolve_url(node.url.to_s, context: context)
|
|
255
|
+
image_style = context.inherit(:image)
|
|
256
|
+
text_style = context.inherit(:image_text)
|
|
257
|
+
alt = render_inlines(children_of(node), context: context.with(current_style: text_style))
|
|
258
|
+
label = if text_style.format.empty?
|
|
259
|
+
"Image: #{UI::Width.strip_ansi(alt)} ->"
|
|
260
|
+
else
|
|
261
|
+
text_style.format.gsub("{{text}}", UI::Width.strip_ansi(alt))
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
image_style.render([label, href].reject(&:empty?).join(" "))
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def render_block_with_style(style, body)
|
|
268
|
+
style.render(style.apply_block_layout(body))
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def table_widths(rows)
|
|
272
|
+
column_count = rows.map(&:length).max || 0
|
|
273
|
+
Array.new(column_count) do |index|
|
|
274
|
+
rows.map { |row| UI::Width.measure(row[index].to_s) }.max || 0
|
|
275
|
+
end
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def table_row(row, widths, style)
|
|
279
|
+
separator = style.column_separator || "|"
|
|
280
|
+
cells = widths.each_with_index.map do |width, index|
|
|
281
|
+
value = row[index].to_s
|
|
282
|
+
" #{value}#{" " * [width - UI::Width.measure(value), 0].max} "
|
|
283
|
+
end
|
|
284
|
+
"#{separator}#{cells.join(separator)}#{separator}"
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def table_separator(widths, style)
|
|
288
|
+
separator = style.column_separator || "|"
|
|
289
|
+
row = style.row_separator || "-"
|
|
290
|
+
"#{separator}#{widths.map { |table_width| row * (table_width + 2) }.join(separator)}#{separator}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def quote_indent_width(style)
|
|
294
|
+
return 0 unless style.indent&.positive?
|
|
295
|
+
|
|
296
|
+
UI::Width.measure((style.indent_token || " ") * style.indent)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def resolve_url(value, context:)
|
|
300
|
+
return value if context.base_url.to_s.empty? || value.empty?
|
|
301
|
+
|
|
302
|
+
uri = URI.parse(value)
|
|
303
|
+
return value if uri.absolute?
|
|
304
|
+
|
|
305
|
+
URI.join(context.base_url, value).to_s
|
|
306
|
+
rescue URI::InvalidURIError
|
|
307
|
+
value
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def children_of(node)
|
|
311
|
+
node.each.to_a
|
|
86
312
|
end
|
|
87
313
|
|
|
88
|
-
# Word-wraps a single *line* to *width* display columns using greedy space-splitting.
|
|
89
314
|
def wrap_line(line, width)
|
|
90
315
|
return line if UI::Width.measure(line) <= width
|
|
91
316
|
|