charming 0.1.1 → 0.1.3
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/README.md +2 -2
- data/lib/charming/application.rb +14 -3
- data/lib/charming/cli.rb +23 -0
- data/lib/charming/controller/class_methods.rb +115 -0
- data/lib/charming/controller/command_palette.rb +135 -0
- data/lib/charming/controller/component_dispatching.rb +81 -0
- data/lib/charming/controller/dispatching.rb +60 -0
- data/lib/charming/controller/focus_management.rb +30 -0
- data/lib/charming/controller/rendering.rb +127 -0
- data/lib/charming/controller/session_state.rb +41 -0
- data/lib/charming/controller/sidebar_navigation.rb +111 -0
- data/lib/charming/controller.rb +35 -559
- data/lib/charming/database_commands.rb +16 -0
- data/lib/charming/database_installer.rb +27 -0
- data/lib/charming/focus.rb +58 -2
- data/lib/charming/generators/app_file_generator.rb +13 -0
- data/lib/charming/generators/app_generator.rb +123 -47
- data/lib/charming/generators/base.rb +26 -0
- data/lib/charming/generators/component_generator.rb +10 -10
- data/lib/charming/generators/controller_generator.rb +22 -11
- data/lib/charming/generators/model_generator.rb +38 -29
- data/lib/charming/generators/name.rb +10 -0
- data/lib/charming/generators/screen_generator.rb +78 -32
- data/lib/charming/generators/templates/app/Gemfile.template +5 -0
- data/lib/charming/generators/templates/app/README.md.template +9 -0
- data/lib/charming/generators/templates/app/Rakefile.template +3 -0
- data/lib/charming/generators/templates/app/application.template +13 -0
- data/lib/charming/generators/templates/app/application_controller.template +19 -0
- data/lib/charming/generators/templates/app/application_record.template +7 -0
- data/lib/charming/generators/templates/app/application_state.template +6 -0
- data/lib/charming/generators/templates/app/database_config.template +12 -0
- data/lib/charming/generators/templates/app/executable.template +7 -0
- data/lib/charming/generators/templates/app/gemspec.template +6 -0
- data/lib/charming/generators/templates/app/home_controller.template +6 -0
- data/lib/charming/generators/templates/app/home_state.template +7 -0
- data/lib/charming/generators/templates/app/keep.template +0 -0
- data/lib/charming/generators/templates/app/layout.template +110 -0
- data/lib/charming/generators/templates/app/root_file.template +20 -0
- data/lib/charming/generators/templates/app/routes.template +5 -0
- data/lib/charming/generators/templates/app/seeds.template +1 -0
- data/lib/charming/generators/templates/app/spec_controller.template +17 -0
- data/lib/charming/generators/templates/app/spec_helper.template +3 -0
- data/lib/charming/generators/templates/app/spec_state.template +17 -0
- data/lib/charming/generators/templates/app/spec_view.template +16 -0
- data/lib/charming/generators/templates/app/version.template +5 -0
- data/lib/charming/generators/templates/app/view.template +21 -0
- data/lib/charming/generators/templates/component/component.rb.template +9 -0
- data/lib/charming/generators/templates/controller/controller.rb.template +6 -0
- data/lib/charming/generators/templates/model/migration.rb.template +9 -0
- data/lib/charming/generators/templates/model/model.rb.template +6 -0
- data/lib/charming/generators/templates/model/spec.rb.template +9 -0
- data/lib/charming/generators/templates/screen/controller.rb.template +7 -0
- data/lib/charming/generators/templates/screen/spec_controller.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_state.rb.template +17 -0
- data/lib/charming/generators/templates/screen/spec_view.rb.template +13 -0
- data/lib/charming/generators/templates/screen/state.rb.template +7 -0
- data/lib/charming/generators/templates/screen/view.rb.template +11 -0
- data/lib/charming/generators/templates/view/view.rb.template +11 -0
- data/lib/charming/generators/view_generator.rb +19 -3
- data/lib/charming/internal/renderer/differential.rb +25 -2
- data/lib/charming/internal/renderer/full_repaint.rb +6 -0
- data/lib/charming/internal/terminal/adapter.rb +29 -3
- data/lib/charming/internal/terminal/key_normalizer.rb +84 -0
- data/lib/charming/internal/terminal/memory_backend.rb +28 -1
- data/lib/charming/internal/terminal/mouse_parser.rb +81 -0
- data/lib/charming/internal/terminal/tty_backend.rb +65 -115
- data/lib/charming/presentation/component.rb +3 -5
- data/lib/charming/presentation/components/activity_indicator.rb +173 -134
- data/lib/charming/presentation/components/command_palette.rb +94 -96
- data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
- data/lib/charming/presentation/components/empty_state.rb +47 -36
- data/lib/charming/presentation/components/form/builder.rb +52 -40
- data/lib/charming/presentation/components/form/confirm.rb +50 -39
- data/lib/charming/presentation/components/form/field.rb +94 -71
- data/lib/charming/presentation/components/form/input.rb +61 -49
- data/lib/charming/presentation/components/form/note.rb +27 -20
- data/lib/charming/presentation/components/form/select.rb +84 -63
- data/lib/charming/presentation/components/form/textarea.rb +67 -53
- data/lib/charming/presentation/components/form.rb +120 -93
- data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
- data/lib/charming/presentation/components/list.rb +123 -97
- data/lib/charming/presentation/components/markdown.rb +21 -17
- data/lib/charming/presentation/components/modal.rb +55 -43
- data/lib/charming/presentation/components/progressbar.rb +61 -50
- data/lib/charming/presentation/components/spinner.rb +35 -27
- data/lib/charming/presentation/components/table.rb +108 -85
- data/lib/charming/presentation/components/text_area.rb +219 -173
- data/lib/charming/presentation/components/text_input.rb +120 -98
- data/lib/charming/presentation/components/viewport.rb +218 -168
- data/lib/charming/presentation/layout/builder.rb +84 -0
- data/lib/charming/presentation/layout/overlay.rb +55 -0
- data/lib/charming/presentation/layout/pane.rb +149 -0
- data/lib/charming/presentation/layout/rect.rb +21 -0
- data/lib/charming/presentation/layout/screen_layout.rb +58 -0
- data/lib/charming/presentation/layout/split.rb +132 -0
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/block_renderers.rb +118 -0
- data/lib/charming/presentation/markdown/inline_renderers.rb +66 -0
- data/lib/charming/presentation/markdown/render_context.rb +20 -0
- data/lib/charming/presentation/markdown/renderer.rb +82 -174
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -44
- data/lib/charming/presentation/markdown.rb +4 -3
- data/lib/charming/presentation/template_view.rb +22 -17
- data/lib/charming/presentation/templates/erb_handler.rb +4 -6
- data/lib/charming/presentation/templates.rb +47 -32
- data/lib/charming/presentation/ui/ansi_codes.rb +87 -0
- data/lib/charming/presentation/ui/ansi_slicer.rb +92 -0
- data/lib/charming/presentation/ui/border.rb +24 -26
- data/lib/charming/presentation/ui/border_painter.rb +56 -0
- data/lib/charming/presentation/ui/canvas.rb +80 -0
- data/lib/charming/presentation/ui/style.rb +170 -205
- data/lib/charming/presentation/ui/theme.rb +133 -135
- data/lib/charming/presentation/ui/width.rb +12 -14
- data/lib/charming/presentation/ui.rb +70 -213
- data/lib/charming/presentation/view.rb +107 -92
- data/lib/charming/runtime.rb +25 -10
- data/lib/charming/tasks/inline_executor.rb +9 -0
- data/lib/charming/tasks/task.rb +3 -0
- data/lib/charming/tasks/threaded_executor.rb +12 -0
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +16 -2
- metadata +60 -10
- data/lib/charming/generators/app_generator/app_spec_templates.rb +0 -90
- data/lib/charming/generators/app_generator/basic_templates.rb +0 -81
- data/lib/charming/generators/app_generator/component_templates.rb +0 -36
- data/lib/charming/generators/app_generator/controller_template.rb +0 -60
- data/lib/charming/generators/app_generator/database_templates.rb +0 -45
- data/lib/charming/generators/app_generator/layout_template.rb +0 -66
- data/lib/charming/generators/app_generator/screen_spec_templates.rb +0 -69
- data/lib/charming/generators/app_generator/state_templates.rb +0 -30
- data/lib/charming/generators/app_generator/view_template.rb +0 -84
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Markdown
|
|
5
|
+
# BlockRenderer dispatches Kramdown block-level elements (paragraph, header, list,
|
|
6
|
+
# code block, etc.) to their individual rendering handlers. Handlers are built once
|
|
7
|
+
# at construction time as a frozen hash of element-type symbols to callables.
|
|
8
|
+
class BlockRenderer
|
|
9
|
+
# *renderer* is the parent Renderer (used to wrap text, render inlines, and look up styles).
|
|
10
|
+
def initialize(renderer:)
|
|
11
|
+
@renderer = renderer
|
|
12
|
+
build_handlers
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Renders *element* using the handler registered for `element.type`. Unknown types
|
|
16
|
+
# fall through to `render_unknown`.
|
|
17
|
+
def render(element, context:)
|
|
18
|
+
handler = @handlers[element.type] || method(:render_unknown)
|
|
19
|
+
handler.call(element, context)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# The frozen hash of element-type → handler mapping.
|
|
25
|
+
attr_reader :handlers
|
|
26
|
+
|
|
27
|
+
# Builds the handler hash. Each handler is a small lambda that calls back into the
|
|
28
|
+
# parent renderer (or one of the private render_* methods below).
|
|
29
|
+
def build_handlers
|
|
30
|
+
r = @renderer
|
|
31
|
+
@handlers = {
|
|
32
|
+
p: ->(element, context) { r.wrap(r.render_inlines(element.children), width: context.width) },
|
|
33
|
+
header: ->(element, context) { send(:render_header, element, context) },
|
|
34
|
+
blockquote: ->(element, context) { send(:render_blockquote, element, context) },
|
|
35
|
+
ul: ->(element, context) { send(:render_list, element, ordered: false, context: context) },
|
|
36
|
+
ol: ->(element, context) { send(:render_list, element, ordered: true, context: context) },
|
|
37
|
+
li: ->(element, context) { r.render_blocks(element.children, list_depth: context.list_depth, width: context.width) },
|
|
38
|
+
codeblock: ->(element, _context) { send(:render_codeblock, element) },
|
|
39
|
+
hr: ->(element, context) { send(:render_rule, width: context.width) },
|
|
40
|
+
blank: ->(_element, _context) {}
|
|
41
|
+
}.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Fallback for unknown block types: wraps the raw value when there are no children,
|
|
45
|
+
# otherwise recurses into the children.
|
|
46
|
+
def render_unknown(element, context)
|
|
47
|
+
return @renderer.wrap(element.value.to_s, width: context.width) if element.children.empty?
|
|
48
|
+
|
|
49
|
+
@renderer.render_blocks(element.children, list_depth: context.list_depth, width: context.width)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Renders a header element, using the `markdown_heading` style for h1 and the
|
|
53
|
+
# `markdown_subheading` style for h2+.
|
|
54
|
+
def render_header(element, context)
|
|
55
|
+
rendered = @renderer.wrap(@renderer.render_inlines(element.children), width: context.width)
|
|
56
|
+
style = if element.options[:level].to_i == 1
|
|
57
|
+
@renderer.style_for(:markdown_heading, fallback: @renderer.theme_style(:title))
|
|
58
|
+
else
|
|
59
|
+
@renderer.style_for(:markdown_subheading, fallback: @renderer.theme_style(:title))
|
|
60
|
+
end
|
|
61
|
+
style.render(rendered)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def render_blockquote(element, context)
|
|
65
|
+
quote_width = context.width ? [context.width - 2, 1].max : nil
|
|
66
|
+
rendered = @renderer.render_blocks(element.children, list_depth: context.list_depth, width: quote_width)
|
|
67
|
+
border = @renderer.style_for(:markdown_quote_border, fallback: @renderer.theme_style(:border)).render("|")
|
|
68
|
+
quote_style = @renderer.style_for(:markdown_quote, fallback: @renderer.theme_style(:muted))
|
|
69
|
+
|
|
70
|
+
rendered.lines(chomp: true).map { |line| "#{border} #{quote_style.render(line)}" }.join("\n")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def render_list(element, ordered:, context:)
|
|
74
|
+
element.children.each_with_index.map do |item, index|
|
|
75
|
+
marker = ordered ? "#{ordered_start(element) + index}." : "-"
|
|
76
|
+
render_list_item(item, marker: marker, context: context)
|
|
77
|
+
end.join("\n")
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_list_item(element, marker:, context:)
|
|
81
|
+
indent = " " * context.list_depth
|
|
82
|
+
first_prefix = "#{indent}#{marker} "
|
|
83
|
+
rest_prefix = "#{indent}#{" " * (marker.length + 1)}"
|
|
84
|
+
item_width = context.width ? [context.width - UI::Width.measure(first_prefix), 1].max : nil
|
|
85
|
+
body = @renderer.render_blocks(element.children, list_depth: context.list_depth + 1, width: item_width)
|
|
86
|
+
|
|
87
|
+
body.lines(chomp: true).each_with_index.map do |line, index|
|
|
88
|
+
"#{index.zero? ? first_prefix : rest_prefix}#{line}"
|
|
89
|
+
end.join("\n")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def ordered_start(element)
|
|
93
|
+
element.options.fetch(:start, 1).to_i
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def render_codeblock(element)
|
|
97
|
+
code = element.value.to_s
|
|
98
|
+
rendered = if @renderer.syntax_highlighting
|
|
99
|
+
SyntaxHighlighter.new(theme: @renderer.theme).render(code, language: code_language(element))
|
|
100
|
+
else
|
|
101
|
+
@renderer.style_for(:markdown_code, fallback: @renderer.theme_style(:warn)).render(code)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
rendered.lines(chomp: true).map { |line| " #{line}" }.join("\n")
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def render_rule(width:)
|
|
108
|
+
@renderer.style_for(:markdown_rule, fallback: @renderer.theme_style(:border)).render("-" * (width || Renderer::DEFAULT_RULE_WIDTH))
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def code_language(element)
|
|
112
|
+
return element.options[:lang] if element.options[:lang]
|
|
113
|
+
|
|
114
|
+
element.attr["class"].to_s[/language-([^\s]+)/, 1]
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Markdown
|
|
5
|
+
# InlineRenderer dispatches Kramdown inline-level elements (text, strong, em,
|
|
6
|
+
# codespan, link, line break, HTML entity) to their individual rendering handlers.
|
|
7
|
+
# Handlers are built once at construction as a frozen hash of element-type symbols
|
|
8
|
+
# to callables.
|
|
9
|
+
class InlineRenderer
|
|
10
|
+
# *renderer* is the parent Renderer (used to render nested inlines and look up styles).
|
|
11
|
+
def initialize(renderer:)
|
|
12
|
+
@renderer = renderer
|
|
13
|
+
build_handlers
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Renders *element* using the handler registered for `element.type`. Unknown types
|
|
17
|
+
# fall through to `render_unknown`.
|
|
18
|
+
def render(element, context:)
|
|
19
|
+
handler = @handlers[element.type] || method(:render_unknown)
|
|
20
|
+
handler.call(element, context)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# The frozen hash of element-type → handler mapping.
|
|
26
|
+
attr_reader :handlers
|
|
27
|
+
|
|
28
|
+
# Builds the handler hash for text, strong, em, codespan, link, br, and entity.
|
|
29
|
+
def build_handlers
|
|
30
|
+
r = @renderer
|
|
31
|
+
@handlers = {
|
|
32
|
+
text: ->(element, _context) { element.value.to_s },
|
|
33
|
+
strong: ->(element, context) { render_styled(element, context, :markdown_strong) { |s| s.bold } },
|
|
34
|
+
em: ->(element, context) { render_styled(element, context, :markdown_emphasis) { |s| s.italic } },
|
|
35
|
+
codespan: ->(element, _context) { r.style_for(:markdown_inline_code, fallback: r.theme_style(:warn)).render(element.value.to_s) },
|
|
36
|
+
a: ->(element, context) { send(:render_link, element, context) },
|
|
37
|
+
br: ->(_element, _context) { "\n" },
|
|
38
|
+
entity: ->(element, _context) { element.value.respond_to?(:char) ? element.value.char : element.value.to_s }
|
|
39
|
+
}.freeze
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Renders a styled inline (strong/em) by first rendering children, then applying
|
|
43
|
+
# the theme style and the block-form (e.g., `bold`/`italic`) decoration.
|
|
44
|
+
def render_styled(element, context, style_name)
|
|
45
|
+
rendered = @renderer.render_inlines(element.children, width: context.width)
|
|
46
|
+
style = @renderer.style_for(style_name, fallback: yield(@renderer.theme_style(:text)))
|
|
47
|
+
style.render(rendered)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Renders a Markdown link as "label <href>" (URL omitted when empty), styled with
|
|
51
|
+
# the markdown_link theme token or the info+underline fallback.
|
|
52
|
+
def render_link(element, context)
|
|
53
|
+
label = @renderer.render_inlines(element.children, width: context.width)
|
|
54
|
+
href = element.attr["href"].to_s
|
|
55
|
+
rendered = href.empty? ? label : "#{label} <#{href}>"
|
|
56
|
+
@renderer.style_for(:markdown_link, fallback: @renderer.theme_style(:info).underline).render(rendered)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Fallback for unknown inline types: returns the value when there are no children,
|
|
60
|
+
# otherwise recurses into the children.
|
|
61
|
+
def render_unknown(element, context)
|
|
62
|
+
element.children.empty? ? element.value.to_s : @renderer.render_inlines(element.children, width: context.width)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Charming
|
|
4
|
+
module Markdown
|
|
5
|
+
# RenderContext carries the state needed to render nested Markdown blocks: the current
|
|
6
|
+
# list nesting depth (used for indentation) and the wrap width.
|
|
7
|
+
RenderContext = Data.define(:list_depth, :width) do
|
|
8
|
+
# Builds a new RenderContext with the given *width* and optional starting *list_depth*.
|
|
9
|
+
def self.from(width:, list_depth: 0)
|
|
10
|
+
new(list_depth: list_depth, width: width)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns a derived context with the list depth incremented by *depth_increment*
|
|
14
|
+
# and the wrap width overridden to *width* (defaults to the current width).
|
|
15
|
+
def nested(depth_increment: 0, width: self.width)
|
|
16
|
+
self.class.new(list_depth: list_depth + depth_increment, width: width)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -3,200 +3,108 @@
|
|
|
3
3
|
require "kramdown"
|
|
4
4
|
|
|
5
5
|
module Charming
|
|
6
|
-
module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
6
|
+
module Markdown
|
|
7
|
+
# Renderer is the top-level Markdown-to-ANSI renderer. Parses the *content* with
|
|
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.
|
|
11
|
+
class Renderer
|
|
12
|
+
# Wrap width used by `render_rule` when no width is otherwise specified.
|
|
13
|
+
DEFAULT_RULE_WIDTH = 40
|
|
14
|
+
|
|
15
|
+
# The Markdown source, configured wrap width, theme, and syntax-highlighting flag.
|
|
16
|
+
attr_reader :content, :width, :theme, :syntax_highlighting
|
|
17
|
+
|
|
18
|
+
# *content* is the Markdown source string. *width* optionally wraps paragraphs to that
|
|
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)
|
|
22
|
+
@content = content
|
|
23
|
+
@width = width
|
|
24
|
+
@theme = theme || UI::Theme.default
|
|
25
|
+
@syntax_highlighting = syntax_highlighting
|
|
26
|
+
end
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
end
|
|
28
|
+
# Parses the content and returns the fully-rendered Markdown as a single string.
|
|
29
|
+
def render
|
|
30
|
+
document = Kramdown::Document.new(content.to_s)
|
|
31
|
+
render_blocks(document.root.children)
|
|
32
|
+
end
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
when :ol
|
|
45
|
-
render_list(element, ordered: true, list_depth: list_depth, width: width)
|
|
46
|
-
when :li
|
|
47
|
-
render_blocks(element.children, list_depth: list_depth, width: width)
|
|
48
|
-
when :codeblock
|
|
49
|
-
render_codeblock(element)
|
|
50
|
-
when :hr
|
|
51
|
-
render_rule(width: width)
|
|
52
|
-
when :blank
|
|
53
|
-
nil
|
|
54
|
-
else
|
|
55
|
-
render_unknown(element, list_depth: list_depth, width: width)
|
|
56
|
-
end
|
|
57
|
-
end
|
|
34
|
+
# Renders a list of Kramdown block *elements* into a string, joined by blank lines.
|
|
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)
|
|
39
|
+
elements.filter_map do |element|
|
|
40
|
+
rendered = block_renderer.render(element, context: context)
|
|
41
|
+
rendered unless rendered.to_s.empty?
|
|
42
|
+
end.join("\n\n")
|
|
43
|
+
end
|
|
58
44
|
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
# Renders a list of Kramdown inline *elements* into a single concatenated string.
|
|
46
|
+
# *width* defaults to the renderer's configured width.
|
|
47
|
+
def render_inlines(elements, width: @width)
|
|
48
|
+
context = RenderContext.from(width: width)
|
|
49
|
+
elements.map { |element| inline_renderer.render(element, context: context) }.join
|
|
50
|
+
end
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
def wrap(value, width:)
|
|
55
|
+
return value unless width
|
|
64
56
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
style = if element.options[:level].to_i == 1
|
|
68
|
-
style_for(:markdown_heading, fallback: theme_style(:title))
|
|
69
|
-
else
|
|
70
|
-
style_for(:markdown_subheading, fallback: theme_style(:title))
|
|
71
|
-
end
|
|
72
|
-
style.render(rendered)
|
|
73
|
-
end
|
|
57
|
+
value.to_s.lines(chomp: true).map { |line| wrap_line(line, width) }.join("\n")
|
|
58
|
+
end
|
|
74
59
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
quote_style = style_for(:markdown_quote, fallback: theme_style(:muted))
|
|
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
|
+
def style_for(name, fallback:)
|
|
63
|
+
return theme.public_send(name) if theme.respond_to?(name)
|
|
80
64
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
end.join("\n")
|
|
84
|
-
end
|
|
65
|
+
fallback
|
|
66
|
+
end
|
|
85
67
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
end.join("\n")
|
|
91
|
-
end
|
|
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
|
+
def theme_style(name)
|
|
71
|
+
return theme.public_send(name) if theme.respond_to?(name)
|
|
92
72
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
first_prefix = "#{indent}#{marker} "
|
|
96
|
-
rest_prefix = "#{indent}#{" " * (marker.length + 1)}"
|
|
97
|
-
item_width = width ? [width - UI::Width.measure(first_prefix), 1].max : nil
|
|
98
|
-
body = render_blocks(element.children, list_depth: list_depth + 1, width: item_width)
|
|
73
|
+
UI::Theme::DEFAULT_TOKENS.fetch(name).then { |token| UI::Theme.new(name => token).public_send(name) }
|
|
74
|
+
end
|
|
99
75
|
|
|
100
|
-
|
|
101
|
-
"#{index.zero? ? first_prefix : rest_prefix}#{line}"
|
|
102
|
-
end.join("\n")
|
|
103
|
-
end
|
|
76
|
+
private
|
|
104
77
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
78
|
+
# The BlockRenderer instance, lazily built.
|
|
79
|
+
def block_renderer
|
|
80
|
+
@block_renderer ||= BlockRenderer.new(renderer: self)
|
|
81
|
+
end
|
|
108
82
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
else
|
|
114
|
-
style_for(:markdown_code, fallback: theme_style(:warn)).render(code)
|
|
115
|
-
end
|
|
83
|
+
# The InlineRenderer instance, lazily built.
|
|
84
|
+
def inline_renderer
|
|
85
|
+
@inline_renderer ||= InlineRenderer.new(renderer: self)
|
|
86
|
+
end
|
|
116
87
|
|
|
117
|
-
|
|
118
|
-
|
|
88
|
+
# Word-wraps a single *line* to *width* display columns using greedy space-splitting.
|
|
89
|
+
def wrap_line(line, width)
|
|
90
|
+
return line if UI::Width.measure(line) <= width
|
|
119
91
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
end
|
|
92
|
+
lines = []
|
|
93
|
+
current = +""
|
|
123
94
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
end
|
|
95
|
+
line.split(/\s+/).each do |word|
|
|
96
|
+
candidate = current.empty? ? word : "#{current} #{word}"
|
|
127
97
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
element.value.to_s
|
|
132
|
-
when :strong
|
|
133
|
-
style_for(:markdown_strong, fallback: theme_style(:text).bold).render(render_inlines(element.children))
|
|
134
|
-
when :em
|
|
135
|
-
style_for(:markdown_emphasis, fallback: theme_style(:text).italic).render(render_inlines(element.children))
|
|
136
|
-
when :codespan
|
|
137
|
-
style_for(:markdown_inline_code, fallback: theme_style(:warn)).render(element.value.to_s)
|
|
138
|
-
when :a
|
|
139
|
-
render_link(element)
|
|
140
|
-
when :br
|
|
141
|
-
"\n"
|
|
142
|
-
when :entity
|
|
143
|
-
element.value.respond_to?(:char) ? element.value.char : element.value.to_s
|
|
98
|
+
if !current.empty? && UI::Width.measure(candidate) > width
|
|
99
|
+
lines << current.rstrip
|
|
100
|
+
current = word
|
|
144
101
|
else
|
|
145
|
-
|
|
102
|
+
current = candidate
|
|
146
103
|
end
|
|
147
104
|
end
|
|
148
105
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
href = element.attr["href"].to_s
|
|
152
|
-
rendered = href.empty? ? label : "#{label} <#{href}>"
|
|
153
|
-
style_for(:markdown_link, fallback: theme_style(:info).underline).render(rendered)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
def code_language(element)
|
|
157
|
-
return element.options[:lang] if element.options[:lang]
|
|
158
|
-
|
|
159
|
-
element.attr["class"].to_s[/language-([^\s]+)/, 1]
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def wrap(value, width:)
|
|
163
|
-
return value unless width
|
|
164
|
-
|
|
165
|
-
value.to_s.lines(chomp: true).map { |line| wrap_line(line, width) }.join("\n")
|
|
166
|
-
end
|
|
167
|
-
|
|
168
|
-
def wrap_line(line, width)
|
|
169
|
-
return line if UI::Width.measure(line) <= width
|
|
170
|
-
|
|
171
|
-
lines = []
|
|
172
|
-
current = +""
|
|
173
|
-
|
|
174
|
-
line.split(/\s+/).each do |word|
|
|
175
|
-
candidate = current.empty? ? word : "#{current} #{word}"
|
|
176
|
-
|
|
177
|
-
if !current.empty? && UI::Width.measure(candidate) > width
|
|
178
|
-
lines << current.rstrip
|
|
179
|
-
current = word
|
|
180
|
-
else
|
|
181
|
-
current = candidate
|
|
182
|
-
end
|
|
183
|
-
end
|
|
184
|
-
|
|
185
|
-
lines << current.rstrip unless current.empty?
|
|
186
|
-
lines.join("\n")
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
def style_for(name, fallback:)
|
|
190
|
-
return theme.public_send(name) if theme.respond_to?(name)
|
|
191
|
-
|
|
192
|
-
fallback
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
def theme_style(name)
|
|
196
|
-
return theme.public_send(name) if theme.respond_to?(name)
|
|
197
|
-
|
|
198
|
-
UI::Theme::DEFAULT_TOKENS.fetch(name).then { |token| UI::Theme.new(name => token).public_send(name) }
|
|
199
|
-
end
|
|
106
|
+
lines << current.rstrip unless current.empty?
|
|
107
|
+
lines.join("\n")
|
|
200
108
|
end
|
|
201
109
|
end
|
|
202
110
|
end
|
|
@@ -3,60 +3,74 @@
|
|
|
3
3
|
require "rouge"
|
|
4
4
|
|
|
5
5
|
module Charming
|
|
6
|
-
module
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
6
|
+
module Markdown
|
|
7
|
+
# SyntaxHighlighter turns a code block string into ANSI-styled terminal text using
|
|
8
|
+
# Rouge lexers. The theme provides markdown_code_* tokens for per-token styling;
|
|
9
|
+
# when a token is undefined in the theme, the highlighter falls back to a sensible
|
|
10
|
+
# base style (muted italic for comments, title for keywords, etc.).
|
|
11
|
+
class SyntaxHighlighter
|
|
12
|
+
# *theme* is the active Charming theme. Defaults to UI::Theme.default.
|
|
13
|
+
def initialize(theme: UI::Theme.default)
|
|
14
|
+
@theme = theme || UI::Theme.default
|
|
15
|
+
end
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
# Highlights *code* (using Rouge) for the given *language* (auto-detected when nil)
|
|
18
|
+
# and returns a styled multi-line string. Each Rouge token is rendered with the
|
|
19
|
+
# theme style matching its token type.
|
|
20
|
+
def render(code, language: nil)
|
|
21
|
+
lexer = lexer_for(language, code)
|
|
22
|
+
lexer.lex(code.to_s).map do |token, value|
|
|
23
|
+
style_for(token).render(value)
|
|
24
|
+
end.join
|
|
25
|
+
end
|
|
19
26
|
|
|
20
|
-
|
|
27
|
+
private
|
|
21
28
|
|
|
22
|
-
|
|
29
|
+
# The Charming theme used for token styling.
|
|
30
|
+
attr_reader :theme
|
|
23
31
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
# Picks a Rouge lexer for *language* and *code*, falling back to plain text.
|
|
33
|
+
def lexer_for(language, code)
|
|
34
|
+
Rouge::Lexer.find_fancy(language, code) || Rouge::Lexers::PlainText
|
|
35
|
+
end
|
|
27
36
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
# Returns the Charming style for a given Rouge *token*, mapping token qualifiers
|
|
38
|
+
# to theme tokens and falling back to a sensible base style per category.
|
|
39
|
+
def style_for(token)
|
|
40
|
+
name = token_name(token)
|
|
41
|
+
|
|
42
|
+
case name
|
|
43
|
+
when /Comment/
|
|
44
|
+
theme_style(:markdown_code_comment, fallback: theme_style(:muted).italic)
|
|
45
|
+
when /Keyword/
|
|
46
|
+
theme_style(:markdown_code_keyword, fallback: theme_style(:title))
|
|
47
|
+
when /String/
|
|
48
|
+
theme_style(:markdown_code_string, fallback: theme_style(:warn))
|
|
49
|
+
when /Number|Literal/
|
|
50
|
+
theme_style(:markdown_code_literal, fallback: theme_style(:info))
|
|
51
|
+
when /Name\.(Class|Constant|Function|Namespace)/
|
|
52
|
+
theme_style(:markdown_code_constant, fallback: theme_style(:info))
|
|
53
|
+
when /Error/
|
|
54
|
+
theme_style(:markdown_code_error, fallback: theme_style(:warn).bold)
|
|
55
|
+
else
|
|
56
|
+
theme_style(:markdown_code, fallback: theme_style(:text))
|
|
47
57
|
end
|
|
58
|
+
end
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
60
|
+
# Returns the qualified token name when the token object supports it, otherwise
|
|
61
|
+
# the token's default `to_s`.
|
|
62
|
+
def token_name(token)
|
|
63
|
+
return token.qualname if token.respond_to?(:qualname)
|
|
51
64
|
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
token.to_s
|
|
66
|
+
end
|
|
54
67
|
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
# Returns the theme's style for *name*, falling back to *fallback* (or a default
|
|
69
|
+
# empty style) when the theme doesn't define it.
|
|
70
|
+
def theme_style(name, fallback: nil)
|
|
71
|
+
return theme.public_send(name) if theme.respond_to?(name)
|
|
57
72
|
|
|
58
|
-
|
|
59
|
-
end
|
|
73
|
+
fallback || UI.style
|
|
60
74
|
end
|
|
61
75
|
end
|
|
62
76
|
end
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
# Markdown is the namespace for the Markdown rendering pipeline. Parsing is delegated to
|
|
5
|
+
# Kramdown; per-block and per-inline element rendering is handled by `BlockRenderer`
|
|
6
|
+
# and `InlineRenderer`; code blocks are highlighted by `SyntaxHighlighter` (Rouge-backed).
|
|
7
|
+
module Markdown
|
|
7
8
|
end
|
|
8
9
|
end
|