Class: Howzit::Topic
Overview
Topic Class
Instance Attribute Summary collapse
-
#arg_definitions ⇒ Object
readonly
Returns the value of attribute arg_definitions.
-
#content ⇒ Object
Returns the value of attribute content.
-
#directives ⇒ Object
readonly
Returns the value of attribute directives.
-
#named_args ⇒ Object
readonly
Returns the value of attribute named_args.
-
#parent ⇒ Object
writeonly
Sets the attribute parent.
-
#postreqs ⇒ Object
readonly
Returns the value of attribute postreqs.
-
#prereqs ⇒ Object
readonly
Returns the value of attribute prereqs.
-
#results ⇒ Object
readonly
Returns the value of attribute results.
-
#source_file ⇒ Object
readonly
Returns the value of attribute source_file.
-
#tasks ⇒ Object
readonly
Returns the value of attribute tasks.
-
#title ⇒ Object
readonly
Returns the value of attribute title.
Instance Method Summary collapse
- #<=>(other) ⇒ Object
-
#arguments(from_cli_snapshot: false) ⇒ Object
Get named arguments from title from_cli_snapshot: use Howzit.cli_topic_positional_args (set once from argv after ‘ – `) so earlier topics’ gather_tasks cannot clobber positional binding.
- #ask_task(task) ⇒ Object
- #check_cols ⇒ Object
- #color_directive_yn(keys) ⇒ Object
- #colored_option(color, topic, keys) ⇒ Object
- #colored_yn(color, default) ⇒ Object
- #define_optional(optional) ⇒ Object
- #define_task_args(keys) ⇒ Object
-
#format_arg_definition(arg) ⇒ String
Format an argument definition with syntax highlighting Parentheses in blue, variable name in bright white, default in yellow.
-
#grep(term) ⇒ Object
Search title and contents for a pattern.
-
#highlight_variables(text) ⇒ String
Highlight variable placeholders in content Format: $variable or $variable:default Dollar sign and braces in blue, variable name in bright white, default in yellow.
-
#initialize(title, content, metadata = nil, source_file: nil) ⇒ Topic
constructor
Initialize a topic object.
-
#print_out(options = {}) ⇒ Array
Output a topic with fancy title and bright white text.
- #process_directive(keys) ⇒ Object
-
#process_include(keys, opt) ⇒ Object
Handle an include statement.
-
#run(nested: false) ⇒ Object
Handle run command, execute directives in topic.
- #title_code_block(keys) ⇒ Object
- #title_option(color, topic, keys, opt) ⇒ Object
Constructor Details
#initialize(title, content, metadata = nil, source_file: nil) ⇒ Topic
Initialize a topic object
20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# File 'lib/howzit/topic.rb', line 20 def initialize(title, content, = nil, source_file: nil) @title = title @content = content @parent = nil @nest_level = 0 @named_args = {} @metadata = @source_file = source_file arguments(from_cli_snapshot: true) @directives = parse_directives_with_conditionals @tasks = gather_tasks @results = { total: 0, success: 0, errors: 0, message: ''.c } end |
Instance Attribute Details
#arg_definitions ⇒ Object (readonly)
Returns the value of attribute arg_definitions.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def arg_definitions @arg_definitions end |
#content ⇒ Object
Returns the value of attribute content.
8 9 10 |
# File 'lib/howzit/topic.rb', line 8 def content @content end |
#directives ⇒ Object (readonly)
Returns the value of attribute directives.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def directives @directives end |
#named_args ⇒ Object (readonly)
Returns the value of attribute named_args.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def named_args @named_args end |
#parent=(value) ⇒ Object (writeonly)
Sets the attribute parent
6 7 8 |
# File 'lib/howzit/topic.rb', line 6 def parent=(value) @parent = value end |
#postreqs ⇒ Object (readonly)
Returns the value of attribute postreqs.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def postreqs @postreqs end |
#prereqs ⇒ Object (readonly)
Returns the value of attribute prereqs.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def prereqs @prereqs end |
#results ⇒ Object (readonly)
Returns the value of attribute results.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def results @results end |
#source_file ⇒ Object (readonly)
Returns the value of attribute source_file.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def source_file @source_file end |
#tasks ⇒ Object (readonly)
Returns the value of attribute tasks.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def tasks @tasks end |
#title ⇒ Object (readonly)
Returns the value of attribute title.
10 11 12 |
# File 'lib/howzit/topic.rb', line 10 def title @title end |
Instance Method Details
#<=>(other) ⇒ Object
376 377 378 |
# File 'lib/howzit/topic.rb', line 376 def <=>(other) @title <=> other.title end |
#arguments(from_cli_snapshot: false) ⇒ Object
Get named arguments from title from_cli_snapshot: use Howzit.cli_topic_positional_args (set once from argv after ‘ – `) so earlier topics’ gather_tasks cannot clobber positional binding. Re-entrant calls (e.g. @include with [a,b]) pass false to use live Howzit.arguments.
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
# File 'lib/howzit/topic.rb', line 38 def arguments(from_cli_snapshot: false) @arg_definitions = [] return unless @title =~ /\(.*?\) *$/ positional = if from_cli_snapshot # Specs / non-CLI: leave unset to keep using Howzit.arguments if Howzit.cli_topic_positional_args.nil? Howzit.arguments || [] else Howzit.cli_topic_positional_args end else Howzit.arguments || [] end a = @title.match(/\((?<args>.*?)\) *$/) args = a['args'].split(/ *, */).each(&:strip) args.each_with_index do |arg, idx| arg_name, default = arg.split(/:/).map(&:strip) # Store original definition for display purposes @arg_definitions << (default ? "#{arg_name}:#{default}" : arg_name) @named_args[arg_name] = if positional && positional.count >= idx + 1 positional[idx] else default end end @title = @title.sub(/\(.*?\) *$/, '').strip end |
#ask_task(task) ⇒ Object
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/howzit/topic.rb', line 80 def ask_task(task) note = if task.type == :include task_count = Howzit.buildnote.find_topic(task.action)[0].tasks.count " (#{task_count} tasks)" else '' end q = %({bg}#{task.type.to_s.capitalize} {xw}"{bw}#{task.title}{xw}"#{note}{x}).c Prompt.yn(q, default: task.default) end |
#check_cols ⇒ Object
91 92 93 94 95 |
# File 'lib/howzit/topic.rb', line 91 def check_cols TTY::Screen.columns > 60 ? 60 : TTY::Screen.columns rescue StandardError 60 end |
#color_directive_yn(keys) ⇒ Object
245 246 247 248 249 250 251 252 |
# File 'lib/howzit/topic.rb', line 245 def color_directive_yn(keys) optional, default = define_optional(keys[:optional]) if optional default ? ' {xk}[{g}Y{xk}/{dbw}n{xk}]{x}'.c : ' {xk}[{dbw}y{xk}/{g}N{xk}]{x}'.c else '' end end |
#colored_option(color, topic, keys) ⇒ Object
179 180 181 182 183 184 185 186 187 188 189 190 191 |
# File 'lib/howzit/topic.rb', line 179 def colored_option(color, topic, keys) if topic.tasks.empty? '' else optional = keys[:optional] =~ /[?!]+/ ? true : false default = keys[:optional] =~ /!/ ? false : true if optional colored_yn(color, default) else '' end end end |
#colored_yn(color, default) ⇒ Object
193 194 195 196 197 198 199 |
# File 'lib/howzit/topic.rb', line 193 def colored_yn(color, default) if default " {xKk}[{gbK}Y{xKk}/{dbwK}n{xKk}]{x}#{color}".c else " {xKk}[{dbwK}y{xKk}/{bgK}N{xKk}]{x}#{color}".c end end |
#define_optional(optional) ⇒ Object
272 273 274 275 276 |
# File 'lib/howzit/topic.rb', line 272 def define_optional(optional) is_optional = optional =~ /[?!]+/ ? true : false default = optional =~ /!/ ? false : true [is_optional, default] end |
#define_task_args(keys) ⇒ Object
380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 |
# File 'lib/howzit/topic.rb', line 380 def define_task_args(keys) cmd = keys[:cmd] obj = keys[:action] # Extract and clean the title raw_title = keys[:title] # Determine the title: use provided title if available, otherwise use action title = if raw_title.nil? || raw_title.to_s.strip.empty? obj else raw_title.to_s.strip end # Store the actual title (not overridden by show_all_code - that's only for display) task_args = { type: :include, arguments: nil, title: title.dup, # Make a copy to avoid reference issues action: obj, parent: self } # Set named_arguments before processing titles for variable substitution # Merge with existing named_arguments to preserve @set_var variables Howzit.named_arguments ||= {} Howzit.named_arguments.merge!(@named_args) if @named_args case cmd when /include/i if title =~ /\[(.*?)\] *$/ args = Regexp.last_match(1).split(/ *, */).map(&:render_arguments) Howzit.arguments = args arguments title.sub!(/ *\[.*?\] *$/, '') end # Apply variable substitution to title after bracket processing task_args[:title] = title.render_arguments task_args[:type] = :include task_args[:arguments] = Howzit.named_arguments when /run/i task_args[:type] = :run task_args[:title] = title.render_arguments # Parse log_level from action if present (format: script, log_level=level) if obj =~ /,\s*log_level\s*=\s*(\w+)/i log_level = Regexp.last_match(1).downcase task_args[:log_level] = log_level # Remove log_level parameter from action obj = obj.sub(/,\s*log_level\s*=\s*\w+/i, '').strip end task_args[:action] = obj when /copy/i task_args[:type] = :copy task_args[:action] = Shellwords.escape(obj) task_args[:title] = title.render_arguments when /open|url/i task_args[:type] = :open task_args[:title] = title.render_arguments end task_args end |
#format_arg_definition(arg) ⇒ String
Format an argument definition with syntax highlighting Parentheses in blue, variable name in bright white, default in yellow
345 346 347 348 349 350 351 352 |
# File 'lib/howzit/topic.rb', line 345 def format_arg_definition(arg) if arg.include?(':') name, default = arg.split(':', 2) "{bw}#{name}{l}:{y}#{default}{x}".c else "{bw}#{arg}{x}".c end end |
#grep(term) ⇒ Object
Search title and contents for a pattern
76 77 78 |
# File 'lib/howzit/topic.rb', line 76 def grep(term) @title =~ /#{term}/i || @content =~ /#{term}/i end |
#highlight_variables(text) ⇒ String
Highlight variable placeholders in content Format: $variable or $variable:default Dollar sign and braces in blue, variable name in bright white, default in yellow
363 364 365 366 367 368 369 370 371 372 373 |
# File 'lib/howzit/topic.rb', line 363 def highlight_variables(text) text.gsub(/\$\{([A-Za-z0-9_]+)(?::([^}]*))?\}/) do var_name = Regexp.last_match(1) default = Regexp.last_match(2) if default "{l}\\$\\{{bw}#{var_name}{l}:{y}#{default}{l}\\}{x}".c else "{l}\\$\\{{bw}#{var_name}{l}\\}{x}".c end end end |
#print_out(options = {}) ⇒ Array
Output a topic with fancy title and bright white text.
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/howzit/topic.rb', line 292 def print_out( = {}) defaults = { single: false, header: true } opt = defaults.merge() output = [] if opt[:header] # Include argument definitions in header if present header_title = @title.dup unless @arg_definitions.nil? || @arg_definitions.empty? formatted_args = @arg_definitions.map { |arg| format_arg_definition(arg) }.join('{l}, '.c) header_title += " {l}({x}#{formatted_args}{l}){x}".c end output.push(header_title.format_header) output.push('') end # Process conditional blocks first = @metadata || Howzit.buildnote&. topic = ConditionalContent.process(@content.dup, { metadata: }) unless Howzit.[:show_all_code] topic.gsub!(/(?mix)^(`{3,})run([?!]*)\s* ([^\n]*)[\s\S]*?\n\1\s*$/, '@@@run\2 \3') end topic.split(/\n/).each do |l| case l when /@(before|after|prereq|end|if|unless)/ next when /@include(?<optional>[!?]{1,2})?\((?<action>[^)]+)\)/ output.concat(process_include(Regexp.last_match.named_captures.symbolize_keys, opt)) when /@(?<cmd>run|copy|open|url)(?<optional>[?!]{1,2})?\((?<action>.*?)\) *(?<title>.*?)$/ output.push(process_directive(Regexp.last_match.named_captures.symbolize_keys)) when /(?<fence>`{3,})run(?<optional>[!?]{1,2})? *(?<title>.*?)$/i desc = title_code_block(Regexp.last_match.named_captures.symbolize_keys) output.push("{bmK}\u{25B6} {bwK}#{desc}{x}\n```".c) when /@@@run(?<optional>[!?]{1,2})? *(?<title>.*?)$/i output.push("{bmK}\u{25B6} {bwK}#{title_code_block(Regexp.last_match.named_captures.symbolize_keys)}{x}".c) else l.wrap!(Howzit.[:wrap]) if Howzit.[:wrap].positive? # Highlight variable placeholders in content output.push(highlight_variables(l)) end end Howzit.named_arguments = @named_args output.push('') end |
#process_directive(keys) ⇒ Object
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/howzit/topic.rb', line 254 def process_directive(keys) cmd = keys[:cmd] obj = keys[:action] title = keys[:title].empty? ? obj : keys[:title].strip title = Howzit.[:show_all_code] ? obj : title option = color_directive_yn(keys) icon = case cmd when 'run' "\u{25B6}" when 'copy' "\u{271A}" when /open|url/ "\u{279A}" end "{bmK}#{icon} {bwK}#{title.preserve_escapes}{x}#{option}".c end |
#process_include(keys, opt) ⇒ Object
Handle an include statement
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 |
# File 'lib/howzit/topic.rb', line 208 def process_include(keys, opt) output = [] if keys[:action] =~ / *\[(.*?)\] *$/ Howzit.named_arguments ||= {} Howzit.named_arguments.merge!(@named_args) if @named_args Howzit.arguments = Regexp.last_match(1).split(/ *, */).map!(&:render_arguments) end matches = Howzit.buildnote.find_topic(keys[:action].sub(/ *\[.*?\] *$/, '')) return [] if matches.empty? topic = matches[0] return [] if topic.nil? rule = '{kKd}' color = '{Kyd}' title = title_option(color, topic, keys, opt) = { color: color, hr: '.', border: rule } output.push("#{'> ' * @nest_level}#{title}".format_header()) unless Howzit.inclusions.include?(topic) if opt[:single] && Howzit.inclusions.include?(topic) output.push("#{'> ' * @nest_level}#{title} included above".format_header()) elsif opt[:single] @nest_level += 1 output.concat(topic.print_out({ single: true, header: false })) output.push("#{'> ' * @nest_level}...".format_header()) @nest_level -= 1 end Howzit.inclusions.push(topic) output end |
#run(nested: false) ⇒ Object
Handle run command, execute directives in topic
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/howzit/topic.rb', line 98 def run(nested: false) output = [] cols = check_cols # Use sequential processing if we have directives with conditionals if @directives && @directives.any?(&:conditional?) return run_sequential(nested: nested, output: output, cols: cols) end # Fall back to old behavior for backward compatibility # Note: @set_var directives are already processed in gather_tasks for non-sequential path # This section is kept for backward compatibility but shouldn't be needed if @tasks.count.positive? unless @prereqs.empty? begin puts TTY::Box.frame("{by}#{@prereqs.join("\n\n").wrap(cols - 4)}{x}".c, width: cols) rescue Errno::EPIPE # Pipe closed, ignore end res = Prompt.yn('Have the above prerequisites been met?', default: true) Process.exit 1 unless res end @tasks.each do |task| next if (task.optional || Howzit.[:ask]) && !ask_task(task) run_output, total, success = task.run output.concat(run_output) @results[:total] += total if success @results[:success] += total else Howzit.console.warn %({bw}\u{2297} {br}Error running task {bw}"#{task.title}"{x}).c @results[:errors] += total break unless Howzit.[:force] end log_task_result(task, success) end total = "{bw}#{@results[:total]}{by} #{@results[:total] == 1 ? 'task' : 'tasks'}".c errors = "{bw}#{@results[:errors]}{by} #{@results[:errors] == 1 ? 'error' : 'errors'}".c @results[:message] += if @results[:errors].zero? "{bg}\u{2713} {by}Ran #{total}{x}".c elsif Howzit.[:force] "{br}\u{2715} {by}Completed #{total} with #{errors}{x}".c else "{br}\u{2715} {by}Ran #{total}, terminated due to error{x}".c end else Howzit.console.warn "{r}--run: No {br}@directive{xr} found in {bw}#{@title}{x}".c end output.push(@results[:message]) if Howzit.[:log_level] < 2 && !nested && !Howzit.[:run] unless @postreqs.empty? begin # Apply variable substitution to postreqs content, then wrap each line individually to preserve structure postreqs_content = @postreqs.join("\n\n").render_arguments wrapped_content = postreqs_content.split(/\n/).map { |line| line.wrap(cols - 4) }.join("\n") puts TTY::Box.frame("{bw}#{wrapped_content}{x}".c, width: cols) rescue Errno::EPIPE # Pipe closed, ignore end end output end |
#title_code_block(keys) ⇒ Object
278 279 280 281 282 283 284 |
# File 'lib/howzit/topic.rb', line 278 def title_code_block(keys) if keys[:title].length.positive? "Block: #{keys[:title]}#{color_directive_yn(keys)}" else "Code Block#{color_directive_yn(keys)}" end end |
#title_option(color, topic, keys, opt) ⇒ Object
174 175 176 177 |
# File 'lib/howzit/topic.rb', line 174 def title_option(color, topic, keys, opt) option = colored_option(color, topic, keys) "#{opt[:single] ? 'From' : 'Include'} #{topic.title}#{option}:" end |