Class: Howzit::Topic

Inherits:
Object
  • Object
show all
Includes:
Comparable
Defined in:
lib/howzit/topic.rb

Overview

Topic Class

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(title, content, metadata = nil, source_file: nil) ⇒ Topic

Initialize a topic object

Parameters:

  • title (String)

    The topic title

  • content (String)

    The raw topic content

  • metadata (Hash) (defaults to: nil)

    Optional metadata hash

  • source_file (String) (defaults to: nil)

    Optional path to the build note file this topic came from



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

  @directives = parse_directives_with_conditionals
  @tasks = gather_tasks
  @results = { total: 0, success: 0, errors: 0, message: ''.c }
end

Instance Attribute Details

#arg_definitionsObject (readonly)

Returns the value of attribute arg_definitions.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def arg_definitions
  @arg_definitions
end

#contentObject

Returns the value of attribute content.



8
9
10
# File 'lib/howzit/topic.rb', line 8

def content
  @content
end

#directivesObject (readonly)

Returns the value of attribute directives.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def directives
  @directives
end

#named_argsObject (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

Parameters:

  • value

    the value to set the attribute parent to.



6
7
8
# File 'lib/howzit/topic.rb', line 6

def parent=(value)
  @parent = value
end

#postreqsObject (readonly)

Returns the value of attribute postreqs.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def postreqs
  @postreqs
end

#prereqsObject (readonly)

Returns the value of attribute prereqs.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def prereqs
  @prereqs
end

#resultsObject (readonly)

Returns the value of attribute results.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def results
  @results
end

#source_fileObject (readonly)

Returns the value of attribute source_file.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def source_file
  @source_file
end

#tasksObject (readonly)

Returns the value of attribute tasks.



10
11
12
# File 'lib/howzit/topic.rb', line 10

def tasks
  @tasks
end

#titleObject (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



363
364
365
# File 'lib/howzit/topic.rb', line 363

def <=>(other)
  @title <=> other.title
end

#argumentsObject

Get named arguments from title



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/howzit/topic.rb', line 36

def arguments
  @arg_definitions = []
  return unless @title =~ /\(.*?\) *$/

  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 Howzit.arguments && Howzit.arguments.count >= idx + 1
                              Howzit.arguments[idx]
                            else
                              default
                            end
  end

  @title = @title.sub(/\(.*?\) *$/, '').strip
end

#ask_task(task) ⇒ Object



67
68
69
70
71
72
73
74
75
76
# File 'lib/howzit/topic.rb', line 67

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_colsObject



78
79
80
81
82
# File 'lib/howzit/topic.rb', line 78

def check_cols
  TTY::Screen.columns > 60 ? 60 : TTY::Screen.columns
rescue StandardError
  60
end

#color_directive_yn(keys) ⇒ Object



232
233
234
235
236
237
238
239
# File 'lib/howzit/topic.rb', line 232

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



166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/howzit/topic.rb', line 166

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



180
181
182
183
184
185
186
# File 'lib/howzit/topic.rb', line 180

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



259
260
261
262
263
# File 'lib/howzit/topic.rb', line 259

def define_optional(optional)
  is_optional = optional =~ /[?!]+/ ? true : false
  default = optional =~ /!/ ? false : true
  [is_optional, default]
end

#define_task_args(keys) ⇒ Object



367
368
369
370
371
372
373
374
375
376
377
378
379
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
# File 'lib/howzit/topic.rb', line 367

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

Parameters:

  • arg (String)

    The argument definition (e.g., “var” or “var:default”)

Returns:

  • (String)

    Colorized argument definition



332
333
334
335
336
337
338
339
# File 'lib/howzit/topic.rb', line 332

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

Parameters:

  • term (String)

    the search pattern



63
64
65
# File 'lib/howzit/topic.rb', line 63

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

Parameters:

  • text (String)

    The text to process

Returns:

  • (String)

    Text with highlighted variables



350
351
352
353
354
355
356
357
358
359
360
# File 'lib/howzit/topic.rb', line 350

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

Output a topic with fancy title and bright white text.

Parameters:

  • options (Hash) (defaults to: {})

    The options

Returns:

  • (Array)

    array of formatted lines



279
280
281
282
283
284
285
286
287
288
289
290
291
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
# File 'lib/howzit/topic.rb', line 279

def print_out(options = {})
  defaults = { single: false, header: true }
  opt = defaults.merge(options)

  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.options[: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.options[:wrap]) if Howzit.options[: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



241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
# File 'lib/howzit/topic.rb', line 241

def process_directive(keys)
  cmd = keys[:cmd]
  obj = keys[:action]
  title = keys[:title].empty? ? obj : keys[:title].strip
  title = Howzit.options[: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

Parameters:

  • keys (Hash)

    The symbolized keys and values from the regex that found the statement

  • opt (Hash)

    Options



195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/howzit/topic.rb', line 195

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)
  options = { color: color, hr: '.', border: rule }

  output.push("#{'> ' * @nest_level}#{title}".format_header(options)) unless Howzit.inclusions.include?(topic)

  if opt[:single] && Howzit.inclusions.include?(topic)
    output.push("#{'> ' * @nest_level}#{title} included above".format_header(options))
  elsif opt[:single]
    @nest_level += 1

    output.concat(topic.print_out({ single: true, header: false }))
    output.push("#{'> ' * @nest_level}...".format_header(options))
    @nest_level -= 1
  end
  Howzit.inclusions.push(topic)

  output
end

#run(nested: false) ⇒ Object

Handle run command, execute directives in topic



85
86
87
88
89
90
91
92
93
94
95
96
97
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
# File 'lib/howzit/topic.rb', line 85

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.options[: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.options[: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.options[: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.options[:log_level] < 2 && !nested && !Howzit.options[: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



265
266
267
268
269
270
271
# File 'lib/howzit/topic.rb', line 265

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



161
162
163
164
# File 'lib/howzit/topic.rb', line 161

def title_option(color, topic, keys, opt)
  option = colored_option(color, topic, keys)
  "#{opt[:single] ? 'From' : 'Include'} #{topic.title}#{option}:"
end