howzit 2.1.35 → 2.1.39

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33c0053826d6797da462f0fdfa4cb505f91fd16fbff68b1feebf4e30b2e20a67
4
- data.tar.gz: e9251bd45bc5ef8dc59a25ce2be80850eba66a3c9fbe9c1786b2f5637d2946e6
3
+ metadata.gz: a934719a022ca2f6713474e797eb1a9662dc884a750c93648d4151506ae41571
4
+ data.tar.gz: f261e5e733bd2d695cc76ce36e74096d357983301156c6b5b2ce5757aa3637eb
5
5
  SHA512:
6
- metadata.gz: 1c6e5cf35607d239357b4848b24893ae2091b04a4d264ed472c8ef0553c4c676c2e6417fb6af8d76e1333af2e5f9f050afd0fa8ec791a0a2ffe023297db63e59
7
- data.tar.gz: d5af3431513b66d9f64b253ab1096470c67075780e893aae909b1991027358ea23ad7c759fc4e5dd0c23b95a4e8587879feb0b0964bb044b37fcfc3ce7354c8b
6
+ metadata.gz: 9d928971e7280979fbfba50eedadedfadcf10ee31a26a4f94169c2cfe987706342fd3b826d781d89f4e3882fd87885e38b070352eae4b5e7a38c7fe06828046c
7
+ data.tar.gz: 72cc6fcc43c31a8d1827a5e421ebe41f06120d1fb0c428bb0339daa3e5c39b8477e769830fc302552723804033502aea5354b62b2e914a4d2e2a8bca9ac7421b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,87 @@
1
+ ### 2.1.39
2
+
3
+ 2026-01-25 07:39
4
+
5
+ #### IMPROVED
6
+
7
+ - The --stack option is now negatable, allowing --no-stack to disable stacking even when :stack: true is set in the configuration file, giving users command-line control over stack mode behavior
8
+ - Howzit.buildnote now always creates a new instance when a specific file is requested, preventing cache-related issues when different files need to be loaded
9
+
10
+ #### FIXED
11
+
12
+ - Template topics now have their template name prefixed to their title (e.g., "github:Update GitHub README"), allowing them to be distinguished from local topics and enabling @include directives to reference them using the template prefix format
13
+ - Build note file selection now prioritizes buildnotes.md and howzit.md over other build note files, preventing test files or other build notes from being incorrectly selected when multiple build note files exist in the same directory
14
+ - Explicit file arguments to BuildNote.new are now properly handled, ensuring that when a specific file is provided it is used correctly instead of falling back to file discovery logic, and stack mode is disabled when an explicit file is provided
15
+
16
+ ### 2.1.38
17
+
18
+ 2026-01-25 07:18
19
+
20
+ #### NEW
21
+
22
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
23
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
24
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
25
+ - Menu display for multiple topic matches now shows abbreviated directory
26
+
27
+ #### IMPROVED
28
+
29
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
30
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
31
+ - Template topics are now explicitly loaded from the main build note (closest file) in stack mode to ensure they are always included
32
+ - Source file paths are now always normalized to absolute paths for consistent directory handling
33
+ - Task execution directory logic now only changes directory in stack mode and correctly identifies template files to avoid changing directory for template-based tasks
34
+
35
+ #### FIXED
36
+
37
+ - Template topics are now correctly loaded in both normal and stack modes, fixing an issue where templates were not appearing when using --stack option
38
+ - Template topic filtering now checks against local topics array instead of instance @topics to prevent templates from being incorrectly filtered out when @topics already contains topics from parent directories
39
+ - Build note file selection now prioritizes buildnotes.md and howzit.md over other build note files, preventing test files or other build notes from being incorrectly selected when multiple build note files exist in the same directory
40
+ - Explicit file arguments to BuildNote.new are now properly handled, ensuring that when a specific file is provided it is used correctly instead of falling back to file discovery logic
41
+ - Template topics now have their template name prefixed to their title (e.g., "github:Update GitHub README"), allowing them to be distinguished from local topics and enabling @include directives to reference them using the template prefix format
42
+
43
+ ### 2.1.37
44
+
45
+ 2026-01-25 07:14
46
+
47
+ #### NEW
48
+
49
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
50
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
51
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
52
+ - Menu display for multiple topic matches now shows abbreviated directory
53
+
54
+ #### IMPROVED
55
+
56
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
57
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
58
+ - Template topics are now explicitly loaded from the main build note (closest file) in stack mode to ensure they are always included
59
+ - Source file paths are now always normalized to absolute paths for consistent directory handling
60
+ - Task execution directory logic now only changes directory in stack mode and correctly identifies template files to avoid changing directory for template-based tasks
61
+
62
+ #### FIXED
63
+
64
+ - Template topics are now correctly loaded in both normal and stack modes, fixing an issue where templates were not appearing when using --stack option
65
+ - Template topic filtering now checks against local topics array instead of instance @topics to prevent templates from being incorrectly filtered out when @topics already contains topics from parent directories
66
+ - Build note file selection now prioritizes buildnotes.md and howzit.md over other build note files, preventing test files or other build notes from being incorrectly selected when multiple build note files exist in the same directory
67
+ - Explicit file arguments to BuildNote.new are now properly handled, ensuring that when a specific file is provided it is used correctly instead of falling back to file discovery logic
68
+
69
+ ### 2.1.36
70
+
71
+ 2026-01-25 06:46
72
+
73
+ #### NEW
74
+
75
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
76
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
77
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
78
+ - Menu display for multiple topic matches now shows abbreviated directory
79
+
80
+ #### IMPROVED
81
+
82
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
83
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
84
+
1
85
  ### 2.1.35
2
86
 
3
87
  2026-01-14 04:36
data/README.md CHANGED
@@ -17,6 +17,8 @@ Howzit is a tool that allows you to keep Markdown-formatted notes about a projec
17
17
  - Use `@include()` to import another topic's tasks
18
18
  - Use fenced code blocks to include/run embedded scripts
19
19
  - Scripts can communicate back to Howzit, sending log messages and setting variables
20
+ - Log helper functions output messages immediately with color-coded output (cyan for info, yellow for warn, red for error, dim for debug)
21
+ - Log helper supports `-r`/`--report` flag (or `report` parameter) for delayed output via communication file
20
22
  - Conditional blocks (`@if`/`@unless`/`@elsif`/`@else`) for conditionally including content and tasks
21
23
  - String comparison operators including fuzzy match (`**=`) for flexible pattern matching
22
24
  - File contents conditions to check file contents in conditional blocks
@@ -25,6 +27,8 @@ Howzit is a tool that allows you to keep Markdown-formatted notes about a projec
25
27
  - Templates for easily including repeat tasks
26
28
  - Grep topics for pattern and choose from matches
27
29
  - Use positional and named variables when executing tasks
30
+ - Topic display shows variable definitions with syntax highlighting (blue parentheses, bright white variable names, yellow defaults)
31
+ - Variable placeholders in content are highlighted to show where substitution occurs
28
32
 
29
33
  ## Getting Started
30
34
 
data/bin/howzit CHANGED
@@ -243,6 +243,10 @@ OptionParser.new do |opts|
243
243
 
244
244
  opts.separator("\n Misc:\n\n") #=================================================================== MISC
245
245
 
246
+ opts.on('--[no-]stack', 'Stack build notes from current directory up to root, with closer files taking precedence') do |s|
247
+ Howzit.options[:stack] = s
248
+ end
249
+
246
250
  opts.on('-h', '--help', 'Display this screen') do
247
251
  Howzit::Util.page opts.to_s
248
252
  Process.exit 0
@@ -13,7 +13,16 @@ module Howzit
13
13
  ## @param file [String] The path to the build note file
14
14
  ##
15
15
  def initialize(file: nil, meta: nil)
16
- file ||= note_file
16
+ # Track if an explicit file was provided
17
+ @explicit_file = file ? File.expand_path(file) : nil
18
+
19
+ # Set @note_file if an explicit file was provided, before calling note_file getter
20
+ if file
21
+ @note_file = File.expand_path(file)
22
+ file = @note_file # Use expanded path for reading
23
+ else
24
+ file = note_file
25
+ end
17
26
 
18
27
  @topics = []
19
28
  create_note(prompt: true) if file.nil?
@@ -216,7 +225,11 @@ module Howzit
216
225
  next if s_out.empty?
217
226
 
218
227
  title = topic.title
219
- title += " {dy}({xy}#{topic.named_args.keys.join(', ')}{dy}){x}" unless topic.named_args.empty?
228
+ # Show argument definitions with colorized formatting
229
+ unless topic.arg_definitions.nil? || topic.arg_definitions.empty?
230
+ formatted_args = topic.arg_definitions.map { |arg| topic.format_arg_definition(arg) }.join('{l}, '.c)
231
+ title += " {l}({x}#{formatted_args}{l}){x}".c
232
+ end
220
233
 
221
234
  output.push("- {g}#{title}{x}".c)
222
235
  output.push(s_out.join("\n"))
@@ -646,6 +659,9 @@ module Howzit
646
659
  template_topics.map do |topic|
647
660
  topic.parent = template
648
661
  topic.content = topic.content.render_template(@metadata)
662
+ # Prefix topic title with template name (e.g., "github:Update GitHub README")
663
+ # unless it already has a prefix
664
+ topic.instance_variable_set(:@title, "#{template}:#{topic.title}") unless topic.title.include?(':')
649
665
  topic
650
666
  end
651
667
  end
@@ -676,6 +692,46 @@ module Howzit
676
692
  buildnotes.reverse
677
693
  end
678
694
 
695
+ ##
696
+ ## Find all build note files from current directory up to root
697
+ ##
698
+ ## @return [Array] Array of build note file paths, closest first
699
+ ##
700
+ def glob_stack
701
+ home = Dir.pwd
702
+ buildnotes = []
703
+ current_dir = home
704
+
705
+ while current_dir != '/' && (current_dir =~ %r{[A-Z]:/}).nil?
706
+ Dir.chdir(current_dir)
707
+ filename = glob_note
708
+ if filename
709
+ note = File.expand_path(filename)
710
+ buildnotes.push(note) if File.exist?(note)
711
+ end
712
+ current_dir = File.dirname(current_dir)
713
+ end
714
+
715
+ Dir.chdir(home)
716
+ buildnotes
717
+ end
718
+
719
+ ##
720
+ ## Get base topic name without variables (case-insensitive)
721
+ ##
722
+ ## @param topic_title [String] The topic title
723
+ ##
724
+ ## @return [String] Base topic name in lowercase
725
+ ##
726
+ def base_topic_name(topic_title)
727
+ # Remove prefixes (e.g., "template:Topic Name" -> "Topic Name")
728
+ # Remove variable definitions in parentheses (e.g., "Deploy (increment:1)" -> "Deploy")
729
+ # Normalize to lowercase for case-insensitive matching
730
+ base = topic_title.to_s
731
+ base = base.sub(/^.+:/, '') if base.include?(':') # Remove prefix if present
732
+ base.sub(/ *\(.*?\) *$/, '').strip.downcase
733
+ end
734
+
679
735
  ##
680
736
  ## Glob current directory for valid build note filenames
681
737
  ## (must start with "build" or "howzit" and have
@@ -684,7 +740,15 @@ module Howzit
684
740
  ## @return [String] file path
685
741
  ##
686
742
  def glob_note
687
- Dir.glob('*.{txt,md,markdown}').select(&:build_note?).min
743
+ files = Dir.glob('*.{txt,md,markdown}').select(&:build_note?)
744
+ return nil if files.empty?
745
+
746
+ # Prioritize standard build note filenames
747
+ priority_files = files.select { |f| f =~ /^(buildnotes|howzit)\./i }
748
+ return priority_files.min unless priority_files.empty?
749
+
750
+ # Otherwise return first alphabetically
751
+ files.min
688
752
  end
689
753
 
690
754
  ##
@@ -798,13 +862,20 @@ module Howzit
798
862
  title = "#{short_path}:#{title}"
799
863
  end
800
864
 
801
- topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata)
865
+ # Ensure source_file is always an absolute path
866
+ source_file = path || note_file
867
+ source_file = File.expand_path(source_file) if source_file
868
+ topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata, source_file: source_file)
802
869
 
803
870
  topics.push(topic)
804
871
  end
805
872
 
806
873
  template_topics.each do |topic|
807
- topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
874
+ # Check against local topics array, not @topics, to avoid filtering out templates
875
+ # when @topics already has topics (e.g., in stack mode)
876
+ topic_base = topic.title.sub(/^.+:/, '').strip.downcase
877
+ exists_in_local = topics.any? { |t| t.title.sub(/^.+:/, '').strip.downcase == topic_base }
878
+ topics.push(topic) unless exists_in_local
808
879
  end
809
880
 
810
881
  topics
@@ -816,16 +887,149 @@ module Howzit
816
887
  ## @param path [String] The build note path
817
888
  ##
818
889
  def read_help(path = nil)
819
- @topics = read_help_file(path)
820
- return unless Howzit.options[:include_upstream]
890
+ # Stack mode only applies when reading the main build note
891
+ # When reading templates or included files, use normal mode to prevent recursion
892
+
893
+ # Check if this is a template file
894
+ is_template = false
895
+ if path && Howzit.config.respond_to?(:template_folder) && Howzit.config.template_folder
896
+ template_folder = File.expand_path(Howzit.config.template_folder)
897
+ file_path = File.expand_path(path)
898
+ is_template = file_path.start_with?(template_folder)
899
+ end
900
+
901
+ # Prevent recursion: if we're already processing stack mode, skip it
902
+ in_stack_mode = Howzit.instance_variable_defined?(:@in_stack_mode) && Howzit.instance_variable_get(:@in_stack_mode)
903
+
904
+ # Use stack mode only if:
905
+ # 1. Stack option is enabled
906
+ # 2. We're not already in stack mode (prevent recursion)
907
+ # 3. We're not reading a template file
908
+ # 4. We're reading the main build note (no specific path provided, or path matches the main note file)
909
+ # 5. An explicit file was not provided (explicit files should not use stack mode)
910
+ main_note = path.nil? || begin
911
+ main_file = note_file
912
+ main_file && File.expand_path(path) == File.expand_path(main_file)
913
+ rescue StandardError
914
+ false
915
+ end
916
+
917
+ # Don't use stack mode if an explicit file was provided
918
+ use_stack = Howzit.options[:stack] && !in_stack_mode && !is_template && main_note && @explicit_file.nil?
919
+
920
+ if use_stack
921
+ # Set flag to prevent recursive stack mode calls
922
+ Howzit.instance_variable_set(:@in_stack_mode, true)
923
+ begin
924
+ # Stack mode: read all build notes from current directory up to root
925
+ stack_files = glob_stack
926
+ @topics = []
927
+ existing_base_names = []
928
+
929
+ # Merge metadata from all stacked files
930
+ # Process from root to closest, with closer directories taking precedence
931
+ stacked_metadata = {}
932
+
933
+ # Read metadata from all files (root to closest)
934
+ # Process in reverse so closer files overwrite parent values
935
+ stack_files.reverse.each do |file|
936
+ file_content = Util.read_file(file)
937
+ next if file_content.nil? || file_content.empty?
938
+
939
+ file_meta = file_content.split(/^#/)[0].strip.metadata
940
+ # Merge metadata: closer directories overwrite parent directories
941
+ # Processing root to closest means closer files overwrite parent values
942
+ next unless file_meta.is_a?(Hash)
943
+
944
+ file_meta.each do |key, value|
945
+ stacked_metadata[key] = value
946
+ end
947
+ end
948
+
949
+ # Get global metadata
950
+ global_meta = {}
951
+ raw_global = Howzit.options[:metadata] || Howzit.options['metadata']
952
+ if raw_global.is_a?(Hash)
953
+ raw_global.each do |k, v|
954
+ global_meta[k.to_s.downcase] = v
955
+ end
956
+ end
821
957
 
822
- unless Howzit.has_read_upstream
823
- upstream_topics = read_upstream
958
+ # Get passed metadata (from meta argument in initialize)
959
+ # Store it before we rebuild @metadata
960
+ original_metadata = @metadata.dup if @metadata.is_a?(Hash)
824
961
 
825
- upstream_topics.each do |topic|
826
- @topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
962
+ # Merge order for final metadata (closest directory takes precedence):
963
+ # 1. Global config metadata (lowest precedence)
964
+ # 2. Stacked metadata from all directories (closer overwrites parent)
965
+ # 3. Metadata passed in via meta argument (e.g., templates) - highest precedence
966
+ final_metadata = global_meta.dup
967
+ final_metadata.merge!(stacked_metadata)
968
+
969
+ # Add back any passed metadata that was in the original @metadata
970
+ # These are keys that exist in original but not in global or stacked
971
+ original_metadata&.each do |key, value|
972
+ next if global_meta.key?(key) || stacked_metadata.key?(key)
973
+
974
+ final_metadata[key] = value
975
+ end
976
+
977
+ @metadata = final_metadata
978
+
979
+ # Set primary note file to the closest one (first in stack)
980
+ # But don't override if an explicit file was provided
981
+ @note_file = stack_files.first if stack_files.any? && @explicit_file.nil?
982
+
983
+ # Read topics from each file, closest first
984
+ stack_files.each do |file|
985
+ file_topics = read_help_file(file)
986
+ file_topics.each do |topic|
987
+ # Ensure topic has the correct source_file set (always absolute path)
988
+ abs_file = File.expand_path(file)
989
+ topic.instance_variable_set(:@source_file, abs_file) unless topic.source_file == abs_file
990
+ base_name = base_topic_name(topic.title)
991
+ # Only add if we haven't seen this base topic name yet
992
+ unless existing_base_names.include?(base_name)
993
+ @topics.push(topic)
994
+ existing_base_names.push(base_name)
995
+ end
996
+ end
997
+ end
998
+
999
+ # Load template topics from the main build note (closest file) only
1000
+ # Templates should only be loaded once, from the main build note
1001
+ if stack_files.any?
1002
+ main_file = stack_files.first
1003
+ main_content = Util.read_file(main_file)
1004
+ if main_content
1005
+ main_template_topics = get_template_topics(main_content)
1006
+ main_template_topics.each do |topic|
1007
+ # Check if a topic with this base name already exists
1008
+ base_name = base_topic_name(topic.title)
1009
+ unless existing_base_names.include?(base_name)
1010
+ @topics.push(topic)
1011
+ existing_base_names.push(base_name)
1012
+ end
1013
+ end
1014
+ end
1015
+ end
1016
+ ensure
1017
+ # Clear the flag when done
1018
+ Howzit.instance_variable_set(:@in_stack_mode, false)
1019
+ end
1020
+ else
1021
+ # Normal mode: read single build note file
1022
+ @topics = read_help_file(path)
1023
+ return unless Howzit.options[:include_upstream]
1024
+
1025
+ unless Howzit.has_read_upstream
1026
+ upstream_topics = read_upstream
1027
+
1028
+ upstream_topics.each do |topic|
1029
+ @topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
1030
+ end
1031
+ Howzit.has_read_upstream = true
827
1032
  end
828
- Howzit.has_read_upstream = true
829
1033
  end
830
1034
 
831
1035
  return unless note_file && @topics.empty?
@@ -968,10 +1172,16 @@ module Howzit
968
1172
  when :all
969
1173
  topic_matches.concat(matches.sort_by(&:title))
970
1174
  else
971
- selected_titles = Prompt.choose(matches.map(&:title), height: :max, query: Howzit.options[:grep])
1175
+ # Format titles with abbreviated paths for menu display
1176
+ titles = matches.map { |topic| format_topic_title_for_menu(topic) }
1177
+ selected_titles = Prompt.choose(titles, height: :max, query: Howzit.options[:grep])
972
1178
  # Convert selected titles back to topic objects
973
1179
  selected_titles.each do |title|
974
- matched_topic = matches.find { |t| t.title == title }
1180
+ # Match by formatted title or base title
1181
+ matched_topic = matches.find do |t|
1182
+ formatted = format_topic_title_for_menu(t)
1183
+ formatted == title || t.title == title || t.title.sub(/^[^:]+:\s*/, '') == title.sub(/^[^:]+:\s*/, '')
1184
+ end
975
1185
  topic_matches.push(matched_topic) if matched_topic
976
1186
  end
977
1187
  end
@@ -1095,6 +1305,46 @@ module Howzit
1095
1305
  all_matches
1096
1306
  end
1097
1307
 
1308
+ ##
1309
+ ## Abbreviate a directory path for menu display
1310
+ ##
1311
+ ## @param source_file [String] Path to the source build note file
1312
+ ## @param current_dir [String] Current working directory (default: Dir.pwd)
1313
+ ##
1314
+ ## @return [String] Last directory name, or nil if same directory
1315
+ ##
1316
+ def abbreviate_path(source_file, current_dir = Dir.pwd)
1317
+ return nil unless source_file
1318
+
1319
+ source_dir = File.dirname(File.expand_path(source_file))
1320
+ current_dir = File.expand_path(current_dir)
1321
+
1322
+ # If same directory, return nil (no prefix needed)
1323
+ return nil if source_dir == current_dir
1324
+
1325
+ # Return just the last directory name
1326
+ File.basename(source_dir)
1327
+ end
1328
+
1329
+ ##
1330
+ ## Format topic title for menu display with abbreviated path
1331
+ ##
1332
+ ## @param topic [Topic] The topic to format
1333
+ ##
1334
+ ## @return [String] Formatted title with abbreviated path prefix
1335
+ ##
1336
+ def format_topic_title_for_menu(topic)
1337
+ return topic.title unless topic.source_file
1338
+
1339
+ abbrev = abbreviate_path(topic.source_file)
1340
+ return topic.title if abbrev.nil?
1341
+
1342
+ # Extract base title (remove existing path prefix if present)
1343
+ base_title = topic.title.sub(/^[^:]+:\s*/, '')
1344
+
1345
+ "#{abbrev}: #{base_title}"
1346
+ end
1347
+
1098
1348
  ##
1099
1349
  ## Resolve fuzzy matches for a search term
1100
1350
  ##
@@ -1116,11 +1366,16 @@ module Howzit
1116
1366
  when :all
1117
1367
  matches
1118
1368
  else
1119
- titles = matches.map(&:title)
1369
+ # Format titles with abbreviated paths for menu display
1370
+ titles = matches.map { |topic| format_topic_title_for_menu(topic) }
1120
1371
  res = Prompt.choose(titles, query: search_term)
1121
1372
  # Convert selected titles back to topic objects from the original matches
1122
1373
  res.flat_map do |title|
1123
- matched_topic = matches.find { |t| t.title == title }
1374
+ # Match by formatted title or base title
1375
+ matched_topic = matches.find do |t|
1376
+ formatted = format_topic_title_for_menu(t)
1377
+ formatted == title || t.title == title || t.title.sub(/^[^:]+:\s*/, '') == title.sub(/^[^:]+:\s*/, '')
1378
+ end
1124
1379
  matched_topic || find_topic(title)[0]
1125
1380
  end.compact
1126
1381
 
data/lib/howzit/config.rb CHANGED
@@ -14,6 +14,7 @@ module Howzit
14
14
  highlight: true,
15
15
  highlighter: 'auto',
16
16
  include_upstream: false,
17
+ stack: false,
17
18
  log_level: 1, # 0: debug, 1: info, 2: warn, 3: error
18
19
  matching: 'partial', # exact, partial, fuzzy, beginswith
19
20
  multiple_matches: 'choose',
@@ -68,7 +69,12 @@ module Howzit
68
69
  ## Initialize a config object
69
70
  ##
70
71
  def initialize
71
- load_options
72
+ @initializing = true
73
+ begin
74
+ load_options
75
+ ensure
76
+ @initializing = false
77
+ end
72
78
  end
73
79
 
74
80
  ##
@@ -95,9 +101,28 @@ module Howzit
95
101
  ## @param filename The filename to test
96
102
  ##
97
103
  def should_ignore(filename)
98
- return false unless File.exist?(ignore_file)
104
+ # Prevent recursion: if we're already loading ignore patterns, skip the check
105
+ return false if defined?(@loading_ignore_patterns) && @loading_ignore_patterns
106
+
107
+ # Don't check the ignore file itself - do this before any file operations
108
+ begin
109
+ ignore_file_path = ignore_file
110
+ return false if filename == ignore_file_path || File.expand_path(filename) == File.expand_path(ignore_file_path)
111
+ rescue StandardError
112
+ # If ignore_file access fails, skip the check to prevent recursion
113
+ return false
114
+ end
115
+
116
+ return false unless File.exist?(ignore_file_path)
117
+
118
+ begin
119
+ @loading_ignore_patterns = true
120
+ @ignore_patterns ||= YAML.load(Util.read_file(ignore_file_path))
121
+ ensure
122
+ @loading_ignore_patterns = false
123
+ end
99
124
 
100
- @ignore_patterns ||= YAML.load(Util.read_file(ignore_file))
125
+ return false unless @ignore_patterns.is_a?(Array)
101
126
 
102
127
  ignore = false
103
128
 
@@ -89,6 +89,9 @@ module Howzit
89
89
  Howzit.named_arguments ||= {}
90
90
  Howzit.named_arguments.merge!(parent.named_args) if parent.named_args
91
91
 
92
+ # Pass source_file from parent topic to task
93
+ task_data[:source_file] = parent.respond_to?(:source_file) ? parent.source_file : nil
94
+
92
95
  case task_type
93
96
  when :block
94
97
  # Block tasks need title rendering