gloo 6.0.1 → 6.2.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +9 -3
  3. data/README.md +39 -7
  4. data/docs/application.md +184 -0
  5. data/docs/getting_started.md +110 -0
  6. data/docs/iterators.md +294 -0
  7. data/docs/language_objects.md +190 -0
  8. data/docs/language_scripting.md +62 -0
  9. data/docs/language_syntax.md +334 -0
  10. data/docs/objects.md +77 -0
  11. data/docs/operators.md +62 -0
  12. data/docs/plugins.md +376 -0
  13. data/docs/verbs.md +64 -0
  14. data/docs/web_app.md +211 -0
  15. data/gloo.gemspec +3 -1
  16. data/lib/VERSION +1 -1
  17. data/lib/VERSION_NOTES +16 -0
  18. data/lib/gloo/app/engine.rb +17 -2
  19. data/lib/gloo/app/log.rb +5 -4
  20. data/lib/gloo/app/platform.rb +34 -2
  21. data/lib/gloo/app/prompt.rb +2 -1
  22. data/lib/gloo/app/settings.rb +25 -15
  23. data/lib/gloo/app/table.rb +7 -3
  24. data/lib/gloo/app/theme.rb +144 -0
  25. data/lib/gloo/core/gloo_system.rb +52 -1
  26. data/lib/gloo/core/invoker.rb +102 -0
  27. data/lib/gloo/core/parser.rb +48 -7
  28. data/lib/gloo/core/tokens.rb +81 -1
  29. data/lib/gloo/docs/doc_data.rb +160 -0
  30. data/lib/gloo/docs/help_shell.rb +332 -0
  31. data/lib/gloo/docs/markdown_renderer.rb +39 -0
  32. data/lib/gloo/expr/call.rb +54 -0
  33. data/lib/gloo/expr/expression.rb +3 -0
  34. data/lib/gloo/objs/basic/alias.rb +53 -0
  35. data/lib/gloo/objs/basic/boolean.rb +30 -0
  36. data/lib/gloo/objs/basic/container.rb +38 -0
  37. data/lib/gloo/objs/basic/decimal.rb +30 -0
  38. data/lib/gloo/objs/basic/integer.rb +58 -0
  39. data/lib/gloo/objs/basic/script.rb +31 -0
  40. data/lib/gloo/objs/basic/string.rb +31 -4
  41. data/lib/gloo/objs/basic/string_msgs.rb +212 -2
  42. data/lib/gloo/objs/basic/text.rb +30 -4
  43. data/lib/gloo/objs/basic/untyped.rb +21 -0
  44. data/lib/gloo/objs/ctrl/each.rb +63 -1
  45. data/lib/gloo/objs/ctrl/function.rb +86 -4
  46. data/lib/gloo/objs/ctrl/repeat.rb +41 -0
  47. data/lib/gloo/objs/dt/date.rb +43 -0
  48. data/lib/gloo/objs/dt/datetime.rb +50 -0
  49. data/lib/gloo/objs/dt/time.rb +43 -0
  50. data/lib/gloo/objs/str_utils/cipher.rb +59 -0
  51. data/lib/gloo/objs/str_utils/outline.rb +65 -1
  52. data/lib/gloo/objs/str_utils/password.rb +56 -0
  53. data/lib/gloo/objs/system/erb.rb +39 -0
  54. data/lib/gloo/objs/system/file_handle.rb +56 -0
  55. data/lib/gloo/objs/system/system.rb +29 -0
  56. data/lib/gloo/objs/web/http_get.rb +35 -0
  57. data/lib/gloo/objs/web/http_post.rb +33 -0
  58. data/lib/gloo/objs/web/json.rb +40 -0
  59. data/lib/gloo/objs/web/uri.rb +40 -0
  60. data/lib/gloo/shell/command_node.rb +39 -0
  61. data/lib/gloo/shell/context.rb +93 -0
  62. data/lib/gloo/shell/runner.rb +315 -0
  63. data/lib/gloo/verbs/break.rb +32 -0
  64. data/lib/gloo/verbs/check.rb +49 -0
  65. data/lib/gloo/verbs/cls.rb +18 -0
  66. data/lib/gloo/verbs/context.rb +47 -0
  67. data/lib/gloo/verbs/create.rb +42 -0
  68. data/lib/gloo/verbs/eval.rb +27 -0
  69. data/lib/gloo/verbs/execute.rb +27 -0
  70. data/lib/gloo/verbs/exists.rb +54 -2
  71. data/lib/gloo/verbs/files.rb +22 -0
  72. data/lib/gloo/verbs/help.rb +46 -178
  73. data/lib/gloo/verbs/if.rb +52 -0
  74. data/lib/gloo/verbs/invoke.rb +88 -33
  75. data/lib/gloo/verbs/list.rb +37 -4
  76. data/lib/gloo/verbs/load.rb +55 -2
  77. data/lib/gloo/verbs/log.rb +44 -0
  78. data/lib/gloo/verbs/move.rb +36 -1
  79. data/lib/gloo/verbs/put.rb +35 -0
  80. data/lib/gloo/verbs/quit.rb +19 -0
  81. data/lib/gloo/verbs/redirect.rb +52 -2
  82. data/lib/gloo/verbs/reload.rb +27 -0
  83. data/lib/gloo/verbs/run.rb +39 -0
  84. data/lib/gloo/verbs/save.rb +26 -0
  85. data/lib/gloo/verbs/show.rb +54 -0
  86. data/lib/gloo/verbs/tell.rb +32 -0
  87. data/lib/gloo/verbs/throw.rb +29 -0
  88. data/lib/gloo/verbs/unless.rb +50 -0
  89. data/lib/gloo/verbs/unload.rb +23 -0
  90. data/lib/gloo/verbs/version.rb +37 -3
  91. data/lib/gloo/verbs/wait.rb +26 -0
  92. data/test.gloo/objs/string.test.gloo +54 -0
  93. data/test.gloo/verbs/invoke.test.gloo +73 -0
  94. metadata +26 -10
  95. data/.DS_Store +0 -0
  96. data/.travis.yml +0 -5
@@ -9,6 +9,15 @@ module Gloo
9
9
  module Core
10
10
  class Tokens
11
11
 
12
+ # Keywords that open an inline function call, eg. invoke( ... )
13
+ # or ~>( ... ). Kept here since Tokens is where they get
14
+ # recognized as a single bounded token; Parser#split_params
15
+ # reuses this same list to avoid mistaking a trailing call for
16
+ # its unrelated trailing-optional-param convention.
17
+ CALL_OPENERS = %w[invoke ~>].freeze
18
+
19
+ QUOTE_CHARS = [ '"', "'" ].freeze
20
+
12
21
  attr_reader :cmd, :tokens
13
22
 
14
23
  # ---------------------------------------------------------------------
@@ -142,8 +151,19 @@ module Gloo
142
151
  #
143
152
  # Create a list of token from the given string.
144
153
  #
154
+ # An inline call (invoke( ... ) / ~>( ... )) is checked for
155
+ # first since it needs to be quote-aware in its own right (a
156
+ # call's args can include a quoted string) - see
157
+ # find_call_range. Falls through to the original quote-then-
158
+ # plain-split handling, unchanged, when there's no call.
159
+ #
145
160
  def tokenize( str )
146
- if str.index( '"' )
161
+ range = find_call_range( str )
162
+ if range
163
+ tokenize( str[ 0...range.first ] ) if range.first.positive?
164
+ @tokens << str[ range ]
165
+ tokenize( str[ range.last + 1..-1 ] ) if range.last + 1 < str.length
166
+ elsif str.index( '"' )
147
167
  i = str.index( '"' )
148
168
  j = str.index( '"', i + 1 )
149
169
  j ||= str.length
@@ -164,6 +184,66 @@ module Gloo
164
184
  end
165
185
  end
166
186
 
187
+ #
188
+ # Find the char range of the first top-level (not inside a
189
+ # quoted string) invoke(...)/~>(...) call in str. Returns nil
190
+ # if there isn't one, or it's never properly closed.
191
+ #
192
+ def find_call_range( str )
193
+ i = 0
194
+ while i < str.length
195
+ ch = str[ i ]
196
+
197
+ if QUOTE_CHARS.include?( ch )
198
+ close = str.index( ch, i + 1 )
199
+ return nil unless close
200
+
201
+ i = close + 1
202
+ next
203
+ end
204
+
205
+ CALL_OPENERS.each do |kw|
206
+ opener = "#{kw}("
207
+ next unless str[ i, opener.length ] == opener
208
+ next unless i.zero? || str[ i - 1 ] =~ /\s/
209
+
210
+ close = find_matching_close_paren( str, i + opener.length - 1 )
211
+ return ( i..close ) if close
212
+ end
213
+
214
+ i += 1
215
+ end
216
+ return nil
217
+ end
218
+
219
+ #
220
+ # Given the index of an open paren, find the index of its
221
+ # balancing close paren - skipping over quoted substrings so
222
+ # a stray paren character inside a quoted arg doesn't throw
223
+ # off the depth count. Returns nil if it's never closed.
224
+ #
225
+ def find_matching_close_paren( str, open_index )
226
+ depth = 0
227
+ i = open_index
228
+ while i < str.length
229
+ ch = str[ i ]
230
+ if QUOTE_CHARS.include?( ch )
231
+ close = str.index( ch, i + 1 )
232
+ return nil unless close
233
+
234
+ i = close + 1
235
+ next
236
+ elsif ch == '('
237
+ depth += 1
238
+ elsif ch == ')'
239
+ depth -= 1
240
+ return i if depth.zero?
241
+ end
242
+ i += 1
243
+ end
244
+ return nil
245
+ end
246
+
167
247
  end
168
248
  end
169
249
  end
@@ -0,0 +1,160 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Documentation data for a primitive: a verb or an object.
5
+ #
6
+ # Renders as plain markdown - no color/ANSI here. Color is applied
7
+ # separately, at display time, by Gloo::Docs::MarkdownRenderer, so
8
+ # this stays reusable wherever the raw markdown is useful (paging,
9
+ # export, tests).
10
+ #
11
+
12
+ module Gloo
13
+ module Docs
14
+ class DocData
15
+
16
+ DESCRIPTION = '## Description'.freeze
17
+ SYNTAX = '## Syntax'.freeze
18
+ PARAMETERS = '## Parameters'.freeze
19
+ CHILDREN = '## Children'.freeze
20
+ MESSAGES = '## Messages'.freeze
21
+ RESULT = '## Result'.freeze
22
+ ERRORS = '## Errors'.freeze
23
+ EXAMPLES = '## Examples'.freeze
24
+ NOTES = '## Notes'.freeze
25
+ FENCE = '```gloo'.freeze
26
+
27
+ attr_accessor :name, :shortcut, :description, :syntax,
28
+ :parameters, :children, :messages, :result, :errors,
29
+ :examples, :notes
30
+
31
+ def initialize( value_hash )
32
+ @name = value_hash[:name]
33
+ @shortcut = value_hash[:shortcut]
34
+ @description = value_hash[:description]
35
+ @syntax = value_hash[:syntax]
36
+ @parameters = value_hash[:parameters]
37
+ @children = value_hash[:children]
38
+ @messages = value_hash[:messages]
39
+ @result = value_hash[:result]
40
+ @errors = value_hash[:errors]
41
+ @examples = value_hash[:examples]
42
+ @notes = value_hash[:notes]
43
+ end
44
+
45
+ #
46
+ # Render the documentation as a markdown string.
47
+ def render
48
+ buf = +''
49
+ add_name buf
50
+ add_description buf
51
+ add_syntax buf
52
+ add_parameters buf
53
+ add_children buf
54
+ add_messages buf
55
+ add_result buf
56
+ add_errors buf
57
+ add_examples buf
58
+ add_notes buf
59
+ buf << "\n"
60
+ return buf
61
+ end
62
+
63
+ #
64
+ # Show the documentation in the terminal.
65
+ def show_in_terminal
66
+ puts render
67
+ end
68
+
69
+ private
70
+
71
+ #
72
+ # Append the name and shortcut, if there is a name.
73
+ def add_name( buf )
74
+ return unless @name
75
+
76
+ buf << "\n# #{@name}\n"
77
+ buf << "#{@shortcut}\n" if @shortcut
78
+ end
79
+
80
+ #
81
+ # Append the description, if there is one.
82
+ def add_description( buf )
83
+ return unless @description
84
+
85
+ buf << "\n#{DESCRIPTION}\n#{@description}\n"
86
+ end
87
+
88
+ #
89
+ # Append the syntax lines, if there are any.
90
+ def add_syntax( buf )
91
+ return unless @syntax
92
+
93
+ buf << "\n#{SYNTAX}\n#{FENCE}\n"
94
+ @syntax.each { |line| buf << "#{line}\n" }
95
+ buf << "```\n"
96
+ end
97
+
98
+ #
99
+ # Append the parameter lines, if there are any.
100
+ def add_parameters( buf )
101
+ return unless @parameters
102
+
103
+ buf << "\n#{PARAMETERS}\n"
104
+ @parameters.each { |line| buf << "- #{line}\n" }
105
+ end
106
+
107
+ #
108
+ # Append the child lines, if there are any.
109
+ def add_children( buf )
110
+ return unless @children
111
+
112
+ buf << "\n#{CHILDREN}\n"
113
+ @children.each { |line| buf << "- #{line}\n" }
114
+ end
115
+
116
+ #
117
+ # Append the message lines, if there are any.
118
+ def add_messages( buf )
119
+ return unless @messages
120
+
121
+ buf << "\n#{MESSAGES}\n"
122
+ @messages.each { |line| buf << "- #{line}\n" }
123
+ end
124
+
125
+ #
126
+ # Append the result, if there is one.
127
+ def add_result( buf )
128
+ return unless @result
129
+
130
+ buf << "\n#{RESULT}\n#{@result}\n"
131
+ end
132
+
133
+ #
134
+ # Append the error lines, if there are any.
135
+ def add_errors( buf )
136
+ return unless @errors
137
+
138
+ buf << "\n#{ERRORS}\n"
139
+ @errors.each { |line| buf << "- #{line}\n" }
140
+ end
141
+
142
+ #
143
+ # Append the example code, if there is any.
144
+ def add_examples( buf )
145
+ return unless @examples
146
+
147
+ buf << "\n#{EXAMPLES}\n#{FENCE}\n#{@examples}\n```\n"
148
+ end
149
+
150
+ #
151
+ # Append the notes, if there are any.
152
+ def add_notes( buf )
153
+ return unless @notes
154
+
155
+ buf << "\n#{NOTES}\n#{@notes}\n"
156
+ end
157
+
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,332 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # The interactive help shell, entered by the `help`/`?` verb.
5
+ # Built on Gloo::Shell::Runner (see lib/gloo/shell/).
6
+ #
7
+ require_relative '../shell/runner'
8
+ require_relative 'markdown_renderer'
9
+
10
+ module Gloo
11
+ module Docs
12
+ class HelpShell < Gloo::Shell::Runner
13
+
14
+ PROMPT = 'help>'.freeze
15
+ NO_DOC_YET = 'No documentation available yet for'.freeze
16
+
17
+ VERB_NAMES = :verb_names
18
+ OBJECT_NAMES = :object_names
19
+ DOC_NAMES = :doc_names
20
+ LIBRARY_NAMES = :library_names
21
+ EXTENSION_NAMES = :extension_names
22
+
23
+ DOCS_DIR = File.expand_path( '../../../docs', __dir__ ).freeze
24
+ README_GLOB = 'README*'.freeze
25
+ NO_README_YET = 'No README found for library'.freeze
26
+
27
+ #
28
+ # Initialize the help shell for the given engine.
29
+ #
30
+ def initialize( engine )
31
+ super( engine, prompt: PROMPT, include_quit: true )
32
+ populate_context
33
+ build_commands
34
+ end
35
+
36
+ # ---------------------------------------------------------------------
37
+ # Root commands - lists (mirrors the old help v/o/s/e/l dispatch)
38
+ # ---------------------------------------------------------------------
39
+
40
+ #
41
+ # List all verbs.
42
+ #
43
+ def cmd_show_verbs( _obj, _context )
44
+ theme = @engine.theme
45
+ data = "\n"
46
+ data << theme.heading( " Verbs (shortcut, name)\n" )
47
+ @engine.dictionary.get_verbs.sort_by( &:keyword ).each do |v|
48
+ cut = theme.accent( v.keyword_shortcut.ljust( 5, ' ' ) )
49
+ name = theme.emphasis( v.keyword.ljust( 20, ' ' ) )
50
+ data << " #{cut} #{name} \n"
51
+ end
52
+ @engine.log.show "#{data}\n"
53
+ end
54
+
55
+ #
56
+ # List all object types.
57
+ #
58
+ def cmd_show_objects( _obj, _context )
59
+ theme = @engine.theme
60
+ data = "\n"
61
+ data << theme.heading( " Objects \n" )
62
+ @engine.dictionary.get_obj_types.sort_by( &:typename ).each do |o|
63
+ if o.short_typename != o.typename
64
+ short = theme.accent( "(#{o.short_typename})" )
65
+ name = "#{theme.emphasis( o.typename )} #{short}"
66
+ else
67
+ name = theme.emphasis( o.typename )
68
+ end
69
+ data << " #{name.ljust( 30, ' ' )}\n"
70
+ end
71
+ @engine.log.show "#{data}\n"
72
+ end
73
+
74
+ #
75
+ # Show application settings.
76
+ #
77
+ def cmd_show_settings( _obj, _context )
78
+ @engine.settings.show
79
+ end
80
+
81
+ #
82
+ # Show the current theme, how to change it, and a color preview
83
+ # of both palettes - handy for checking how they actually render
84
+ # in whatever terminal you're sitting in.
85
+ #
86
+ def cmd_show_theme( _obj, _context )
87
+ theme = @engine.theme
88
+ settings = @engine.settings
89
+ config_file = File.join( settings.config_path, 'gloo.yml' )
90
+
91
+ data = "\n"
92
+ data << theme.heading( " Theme\n" )
93
+ data << " Current theme: #{theme.emphasis( settings.theme )}\n\n"
94
+ data << theme.muted( " Change it in #{config_file}:\n" )
95
+ data << theme.muted( " theme: dark (or: light)\n\n" )
96
+ data << theme_preview( ' Dark palette', Gloo::App::Theme.new( 'dark' ) )
97
+ data << "\n"
98
+ data << theme_preview( ' Light palette', Gloo::App::Theme.new( 'light' ) )
99
+ @engine.log.show "#{data}\n"
100
+ end
101
+
102
+ #
103
+ # List loaded extensions.
104
+ #
105
+ def cmd_show_extensions( _obj, _context )
106
+ theme = @engine.theme
107
+ data = "\n"
108
+ data << theme.heading( " Extensions\n" )
109
+ data << theme.muted(
110
+ " Use `load ext {name}` to load a User Extension, \n" \
111
+ " then `object {name}` / `verb {name}` / `extension {name}` here to see what it adds. \n" \
112
+ " Only loaded extensions are listed below.\n\n" )
113
+ loaded = @engine.ext_manager.loaded_extensions
114
+ if loaded.empty?
115
+ data << theme.muted( " (none loaded)\n" )
116
+ else
117
+ loaded.sort.each do |name, _ext|
118
+ data << " #{theme.emphasis( name )} \n"
119
+ end
120
+ end
121
+ @engine.log.show "#{data}\n"
122
+ end
123
+
124
+ #
125
+ # List loaded libraries.
126
+ #
127
+ def cmd_show_libraries( _obj, _context )
128
+ theme = @engine.theme
129
+ data = "\n"
130
+ data << theme.heading( " Libraries\n" )
131
+ data << theme.muted(
132
+ " Use `load lib {name}` to load a core library, \n" \
133
+ " then `object {name}` / `verb {name}` here to see what it adds. \n" \
134
+ " Only loaded libraries are listed below.\n\n" )
135
+ loaded = @engine.lib_manager.loaded_libraries
136
+ if loaded.empty?
137
+ data << theme.muted( " (none loaded)\n" )
138
+ else
139
+ loaded.sort.each do |name, _lib|
140
+ data << " #{theme.emphasis( name )} \n"
141
+ end
142
+ end
143
+ @engine.log.show "#{data}\n"
144
+ end
145
+
146
+ #
147
+ # List all narrative doc pages (dev/gloo/docs/*.md).
148
+ #
149
+ def cmd_show_docs( _obj, _context )
150
+ theme = @engine.theme
151
+ data = "\n"
152
+ data << theme.heading( " Docs\n" )
153
+ doc_page_names.each do |name|
154
+ data << " #{theme.emphasis( name )}\n"
155
+ end
156
+ @engine.log.show "#{data}\n"
157
+ end
158
+
159
+ # ---------------------------------------------------------------------
160
+ # Detail commands - verb {name}, object {name}, doc {name},
161
+ # library {name}
162
+ # ---------------------------------------------------------------------
163
+
164
+ #
165
+ # Show detailed help for one verb.
166
+ #
167
+ def cmd_show_verb_detail( obj, _context )
168
+ verb_class = @engine.dictionary.find_verb( obj )
169
+ return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless verb_class.respond_to?( :doc_data )
170
+
171
+ page_markdown( Gloo::Docs::DocData.new( verb_class.doc_data ).render )
172
+ end
173
+
174
+ #
175
+ # Show detailed help for one object type.
176
+ #
177
+ def cmd_show_object_detail( obj, _context )
178
+ obj_class = @engine.dictionary.find_obj( obj )
179
+ return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless obj_class.respond_to?( :doc_data )
180
+
181
+ page_markdown( Gloo::Docs::DocData.new( obj_class.doc_data ).render )
182
+ end
183
+
184
+ #
185
+ # Show one narrative doc page (dev/gloo/docs/{name}.md).
186
+ #
187
+ def cmd_show_doc_detail( obj, _context )
188
+ path = File.join( DOCS_DIR, "#{obj}.md" )
189
+ return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless File.exist?( path )
190
+
191
+ page_markdown( File.read( path ) )
192
+ end
193
+
194
+ #
195
+ # Show the README for one loaded core library (from the root of
196
+ # its installed gem). Only loaded libraries are tab-completable
197
+ # here - see cmd_show_libraries.
198
+ #
199
+ def cmd_show_library_detail( obj, _context )
200
+ gem_name = @engine.lib_manager.loaded_libraries[ obj ]
201
+ return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless gem_name
202
+
203
+ readme_path = find_readme( gem_name )
204
+ return @engine.log.show "#{NO_README_YET} '#{obj}' (#{gem_name})." unless readme_path
205
+
206
+ page_markdown( File.read( readme_path ) )
207
+ end
208
+
209
+ #
210
+ # Show the README for one loaded user extension (from the root of
211
+ # its extension folder, e.g. ~/gloo/extensions/{name}). Only loaded
212
+ # extensions are tab-completable here - see cmd_show_extensions.
213
+ #
214
+ def cmd_show_extension_detail( obj, _context )
215
+ start_file = @engine.ext_manager.loaded_extensions[ obj ]
216
+ return @engine.log.show "#{NO_DOC_YET} '#{obj}'." unless start_file
217
+
218
+ readme_path = find_extension_readme( start_file )
219
+ return @engine.log.show "#{NO_README_YET} '#{obj}'." unless readme_path
220
+
221
+ page_markdown( File.read( readme_path ) )
222
+ end
223
+
224
+ # ---------------------------------------------------------------------
225
+ # Private
226
+ # ---------------------------------------------------------------------
227
+
228
+ private
229
+
230
+ #
231
+ # Build a labeled preview block for one theme's palette: one line
232
+ # per role, colored in that role's own color, labeled with the
233
+ # role's name. Padding is applied to the plain label before it's
234
+ # colorized, so the ANSI codes don't throw off the alignment.
235
+ #
236
+ def theme_preview( title, theme )
237
+ lines = "#{theme.heading( title )}\n"
238
+ Gloo::App::Theme::ROLES.each do |role|
239
+ label = role.to_s.ljust( 12 )
240
+ lines << theme.send( role, " #{label}#{role}" )
241
+ lines << "\n"
242
+ end
243
+ return lines
244
+ end
245
+
246
+ #
247
+ # Colorize markdown and page it, bracketed with a '---' rule above
248
+ # and below so the content stands out from surrounding CLI output.
249
+ #
250
+ def page_markdown( md )
251
+ rule = '-' * @engine.platform.cols
252
+ bracketed = "#{rule}\n#{md.strip}\n#{rule}\n"
253
+ @engine.platform.page( Gloo::Docs::MarkdownRenderer.colorize( bracketed, @engine.theme ) )
254
+ end
255
+
256
+ #
257
+ # Snapshot the verb, object type, doc page, and loaded library
258
+ # names for tab-completion.
259
+ #
260
+ def populate_context
261
+ set_context( VERB_NAMES, @engine.dictionary.get_verbs.map( &:keyword ).sort )
262
+ set_context( OBJECT_NAMES, @engine.dictionary.get_obj_types.map( &:typename ).sort )
263
+ set_context( DOC_NAMES, doc_page_names )
264
+ set_context( LIBRARY_NAMES, @engine.lib_manager.loaded_libraries.keys.sort )
265
+ set_context( EXTENSION_NAMES, @engine.ext_manager.loaded_extensions.keys.sort )
266
+ end
267
+
268
+ #
269
+ # Find the README file at the root of an installed gem, if any.
270
+ #
271
+ def find_readme( gem_name )
272
+ spec = Gem::Specification.find_by_name( gem_name )
273
+ return Dir.glob( File.join( spec.gem_dir, README_GLOB ) ).first
274
+ rescue Gem::MissingSpecError
275
+ return nil
276
+ end
277
+
278
+ #
279
+ # Find the README file at the root of an extension's folder, given
280
+ # the full path to its {name}_ext.rb start file.
281
+ #
282
+ def find_extension_readme( start_file )
283
+ root = File.dirname( start_file )
284
+ return Dir.glob( File.join( root, README_GLOB ) ).first
285
+ end
286
+
287
+ #
288
+ # List the narrative doc page names (dev/gloo/docs/*.md, without extension).
289
+ #
290
+ def doc_page_names
291
+ return Dir.glob( File.join( DOCS_DIR, '*.md' ) ).map { |f| File.basename( f, '.md' ) }.sort
292
+ end
293
+
294
+ #
295
+ # Build the help shell's command tree.
296
+ #
297
+ def build_commands
298
+ add_command_node(
299
+ name: 'verbs', description: 'List all verbs', method: 'cmd_show_verbs' )
300
+ add_command_node(
301
+ name: 'objects', description: 'List all object types', method: 'cmd_show_objects' )
302
+ add_command_node(
303
+ name: 'settings', description: 'Show application settings', method: 'cmd_show_settings' )
304
+ add_command_node(
305
+ name: 'theme', description: 'Show the current theme and preview both palettes',
306
+ method: 'cmd_show_theme' )
307
+ add_command_node(
308
+ name: 'extensions', description: 'List loaded extensions', method: 'cmd_show_extensions' )
309
+ add_command_node(
310
+ name: 'libraries', description: 'List loaded libraries', method: 'cmd_show_libraries' )
311
+ add_command_node(
312
+ name: 'docs', description: 'List all narrative doc pages', method: 'cmd_show_docs' )
313
+ add_command_node(
314
+ name: 'verb', description: 'Show detailed help for a verb',
315
+ dynamic: true, source: VERB_NAMES, child_method: 'cmd_show_verb_detail' )
316
+ add_command_node(
317
+ name: 'object', description: 'Show detailed help for an object type',
318
+ dynamic: true, source: OBJECT_NAMES, child_method: 'cmd_show_object_detail' )
319
+ add_command_node(
320
+ name: 'doc', description: 'Show one narrative doc page',
321
+ dynamic: true, source: DOC_NAMES, child_method: 'cmd_show_doc_detail' )
322
+ add_command_node(
323
+ name: 'library', description: "Show a loaded library's README",
324
+ dynamic: true, source: LIBRARY_NAMES, child_method: 'cmd_show_library_detail' )
325
+ add_command_node(
326
+ name: 'extension', description: "Show a loaded extension's README",
327
+ dynamic: true, source: EXTENSION_NAMES, child_method: 'cmd_show_extension_detail' )
328
+ end
329
+
330
+ end
331
+ end
332
+ end
@@ -0,0 +1,39 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # Lightly colorize markdown for terminal display: headings and code
5
+ # fences get ANSI color, everything else passes through unchanged.
6
+ # Not a full markdown renderer - just enough that the raw
7
+ # `#`/```` ``` ```` syntax doesn't have to be read literally.
8
+ #
9
+ # Shared by the narrative doc pages (dev/gloo/docs/*.md) and the
10
+ # verb/object detail pages (Gloo::Docs::DocData#render), so both are
11
+ # paged through the same visual style.
12
+ #
13
+ module Gloo
14
+ module Docs
15
+ class MarkdownRenderer
16
+
17
+ #
18
+ # Colorize the given markdown text for terminal display.
19
+ def self.colorize( text, theme )
20
+ in_code = false
21
+ lines = text.split( "\n" ).map do |line|
22
+ if line.start_with?( '```' )
23
+ in_code = !in_code
24
+ next theme.muted( line )
25
+ end
26
+ next theme.muted( line ) if in_code
27
+ next theme.heading( line.sub( /^#\s*/, '' ) ) if line.start_with?( '# ' )
28
+ next theme.subheading( line.sub( /^##\s*/, '' ) ) if line.start_with?( '## ' )
29
+ next theme.subheading( line ) if line.start_with?( '### ' ) || line.start_with?( '#### ' )
30
+ next theme.muted( line ) if line =~ /\A-{3,}\z/
31
+
32
+ line
33
+ end
34
+ return lines.join( "\n" )
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,54 @@
1
+ # Author:: Eric Crane (mailto:eric.crane@mac.com)
2
+ # Copyright:: Copyright (c) 2026 Eric Crane. All rights reserved.
3
+ #
4
+ # An inline function call inside an expression, eg.
5
+ # invoke( functions.add 3 4 ) or ~>( functions.add 3 4 ).
6
+ #
7
+ # Gloo::Core::Tokens recognizes the whole invoke(...)/~>(...) span
8
+ # as one token; Call takes that single token, splits it back out
9
+ # into a target path and raw arg tokens (reusing Tokens itself, so
10
+ # quoted args and nested calls tokenize the same way they would
11
+ # anywhere else), and hands both to Gloo::Core::Invoker - the same
12
+ # resolve/validate/invoke path the standalone invoke verb uses.
13
+ #
14
+
15
+ module Gloo
16
+ module Expr
17
+ class Call
18
+
19
+ attr_reader :target, :arg_tokens
20
+
21
+ #
22
+ # Does the given token look like an inline call?
23
+ #
24
+ def self.call?( token )
25
+ return false unless token.is_a?( String )
26
+
27
+ return Gloo::Core::Tokens::CALL_OPENERS.any? do |kw|
28
+ token.start_with?( "#{kw}(" ) && token.end_with?( ')' )
29
+ end
30
+ end
31
+
32
+ #
33
+ # Parse the call token into a target path and raw arg tokens.
34
+ #
35
+ def initialize( engine, token )
36
+ @engine = engine
37
+
38
+ inner = token[ token.index( '(' ) + 1..-2 ].to_s
39
+ tokens = Gloo::Core::Tokens.new( inner )
40
+ @target = tokens.first
41
+ @arg_tokens = tokens.params || []
42
+ end
43
+
44
+ #
45
+ # Resolve, validate and invoke - returns the result value, or
46
+ # nil if it failed (an error will already have been reported).
47
+ #
48
+ def value
49
+ return Gloo::Core::Invoker.invoke( @engine, @target, @arg_tokens )
50
+ end
51
+
52
+ end
53
+ end
54
+ end