howzit 2.1.35 → 2.1.38

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: 3c0d8cd59db6974b2ceb826ab199eb7eab680919de813f2b2fe4f205c5506697
4
+ data.tar.gz: 1aa7e1e3d80fb27efa4e38d56e5d31d0643871418a80a1417ae045839e6989c1
5
5
  SHA512:
6
- metadata.gz: 1c6e5cf35607d239357b4848b24893ae2091b04a4d264ed472c8ef0553c4c676c2e6417fb6af8d76e1333af2e5f9f050afd0fa8ec791a0a2ffe023297db63e59
7
- data.tar.gz: d5af3431513b66d9f64b253ab1096470c67075780e893aae909b1991027358ea23ad7c759fc4e5dd0c23b95a4e8587879feb0b0964bb044b37fcfc3ce7354c8b
6
+ metadata.gz: a8c95b615947429c402ad32c218d744c69b2d7f51ed50957479471217608e38f04abcdbd4ce95ec551df2546d80f4df1a6b8806772bb6b048b4851bb4211b8e8
7
+ data.tar.gz: 602bdf590e46d40d647ccf8be87dc61de0f86b2b3bafc9c0fe9209d52a648917f798918e1cfd0b3f9f438013e1a61b8eb81915bcfb27e57c3034276f30464aa5
data/CHANGELOG.md CHANGED
@@ -1,3 +1,72 @@
1
+ ### 2.1.38
2
+
3
+ 2026-01-25 07:18
4
+
5
+ #### NEW
6
+
7
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
8
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
9
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
10
+ - Menu display for multiple topic matches now shows abbreviated directory
11
+
12
+ #### IMPROVED
13
+
14
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
15
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
16
+ - Template topics are now explicitly loaded from the main build note (closest file) in stack mode to ensure they are always included
17
+ - Source file paths are now always normalized to absolute paths for consistent directory handling
18
+ - Task execution directory logic now only changes directory in stack mode and correctly identifies template files to avoid changing directory for template-based tasks
19
+
20
+ #### FIXED
21
+
22
+ - Template topics are now correctly loaded in both normal and stack modes, fixing an issue where templates were not appearing when using --stack option
23
+ - 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
24
+ - 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
25
+ - 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
26
+ - 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
27
+
28
+ ### 2.1.37
29
+
30
+ 2026-01-25 07:14
31
+
32
+ #### NEW
33
+
34
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
35
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
36
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
37
+ - Menu display for multiple topic matches now shows abbreviated directory
38
+
39
+ #### IMPROVED
40
+
41
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
42
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
43
+ - Template topics are now explicitly loaded from the main build note (closest file) in stack mode to ensure they are always included
44
+ - Source file paths are now always normalized to absolute paths for consistent directory handling
45
+ - Task execution directory logic now only changes directory in stack mode and correctly identifies template files to avoid changing directory for template-based tasks
46
+
47
+ #### FIXED
48
+
49
+ - Template topics are now correctly loaded in both normal and stack modes, fixing an issue where templates were not appearing when using --stack option
50
+ - 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
51
+ - 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
52
+ - 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
53
+
54
+ ### 2.1.36
55
+
56
+ 2026-01-25 06:46
57
+
58
+ #### NEW
59
+
60
+ - Added --stack option to merge build notes from directory hierarchy up to root, with topics from closer directories taking precedence over parent directories
61
+ - Metadata from all stacked build note files is now merged, with closer directories overriding parent values but parent directories filling in missing keys
62
+ - Commands now execute from the directory where their build note file is located, automatically changing directory before execution and restoring original directory after completion
63
+ - Menu display for multiple topic matches now shows abbreviated directory
64
+
65
+ #### IMPROVED
66
+
67
+ - Topic and Task classes now track source_file to enable directory-aware execution and abbreviated path display
68
+ - Menu selection now handles both abbreviated and full path formats for backward compatibility when matching selected topics
69
+
1
70
  ### 2.1.35
2
71
 
3
72
  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('--stack', 'Stack build notes from current directory up to root, with closer files taking precedence') do
247
+ Howzit.options[:stack] = true
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,12 @@ 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
+ # Set @note_file if an explicit file was provided, before calling note_file getter
17
+ if file
18
+ @note_file = File.expand_path(file)
19
+ else
20
+ file = note_file
21
+ end
17
22
 
18
23
  @topics = []
19
24
  create_note(prompt: true) if file.nil?
@@ -216,7 +221,11 @@ module Howzit
216
221
  next if s_out.empty?
217
222
 
218
223
  title = topic.title
219
- title += " {dy}({xy}#{topic.named_args.keys.join(', ')}{dy}){x}" unless topic.named_args.empty?
224
+ # Show argument definitions with colorized formatting
225
+ unless topic.arg_definitions.nil? || topic.arg_definitions.empty?
226
+ formatted_args = topic.arg_definitions.map { |arg| topic.format_arg_definition(arg) }.join('{l}, '.c)
227
+ title += " {l}({x}#{formatted_args}{l}){x}".c
228
+ end
220
229
 
221
230
  output.push("- {g}#{title}{x}".c)
222
231
  output.push(s_out.join("\n"))
@@ -646,6 +655,11 @@ module Howzit
646
655
  template_topics.map do |topic|
647
656
  topic.parent = template
648
657
  topic.content = topic.content.render_template(@metadata)
658
+ # Prefix topic title with template name (e.g., "github:Update GitHub README")
659
+ # unless it already has a prefix
660
+ unless topic.title.include?(':')
661
+ topic.instance_variable_set(:@title, "#{template}:#{topic.title}")
662
+ end
649
663
  topic
650
664
  end
651
665
  end
@@ -676,6 +690,46 @@ module Howzit
676
690
  buildnotes.reverse
677
691
  end
678
692
 
693
+ ##
694
+ ## Find all build note files from current directory up to root
695
+ ##
696
+ ## @return [Array] Array of build note file paths, closest first
697
+ ##
698
+ def glob_stack
699
+ home = Dir.pwd
700
+ buildnotes = []
701
+ current_dir = home
702
+
703
+ while current_dir != '/' && (current_dir =~ %r{[A-Z]:/}).nil?
704
+ Dir.chdir(current_dir)
705
+ filename = glob_note
706
+ if filename
707
+ note = File.expand_path(filename)
708
+ buildnotes.push(note) if File.exist?(note)
709
+ end
710
+ current_dir = File.dirname(current_dir)
711
+ end
712
+
713
+ Dir.chdir(home)
714
+ buildnotes
715
+ end
716
+
717
+ ##
718
+ ## Get base topic name without variables (case-insensitive)
719
+ ##
720
+ ## @param topic_title [String] The topic title
721
+ ##
722
+ ## @return [String] Base topic name in lowercase
723
+ ##
724
+ def base_topic_name(topic_title)
725
+ # Remove prefixes (e.g., "template:Topic Name" -> "Topic Name")
726
+ # Remove variable definitions in parentheses (e.g., "Deploy (increment:1)" -> "Deploy")
727
+ # Normalize to lowercase for case-insensitive matching
728
+ base = topic_title.to_s
729
+ base = base.sub(/^.+:/, '') if base.include?(':') # Remove prefix if present
730
+ base.sub(/ *\(.*?\) *$/, '').strip.downcase
731
+ end
732
+
679
733
  ##
680
734
  ## Glob current directory for valid build note filenames
681
735
  ## (must start with "build" or "howzit" and have
@@ -684,7 +738,15 @@ module Howzit
684
738
  ## @return [String] file path
685
739
  ##
686
740
  def glob_note
687
- Dir.glob('*.{txt,md,markdown}').select(&:build_note?).min
741
+ files = Dir.glob('*.{txt,md,markdown}').select(&:build_note?)
742
+ return nil if files.empty?
743
+
744
+ # Prioritize standard build note filenames
745
+ priority_files = files.select { |f| f =~ /^(buildnotes|howzit)\./i }
746
+ return priority_files.min unless priority_files.empty?
747
+
748
+ # Otherwise return first alphabetically
749
+ files.min
688
750
  end
689
751
 
690
752
  ##
@@ -798,13 +860,20 @@ module Howzit
798
860
  title = "#{short_path}:#{title}"
799
861
  end
800
862
 
801
- topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata)
863
+ # Ensure source_file is always an absolute path
864
+ source_file = path || note_file
865
+ source_file = File.expand_path(source_file) if source_file
866
+ topic = Topic.new(title, prefix + lines.join("\n").strip.render_template(@metadata), @metadata, source_file: source_file)
802
867
 
803
868
  topics.push(topic)
804
869
  end
805
870
 
806
871
  template_topics.each do |topic|
807
- topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
872
+ # Check against local topics array, not @topics, to avoid filtering out templates
873
+ # when @topics already has topics (e.g., in stack mode)
874
+ topic_base = topic.title.sub(/^.+:/, '').strip.downcase
875
+ exists_in_local = topics.any? { |t| t.title.sub(/^.+:/, '').strip.downcase == topic_base }
876
+ topics.push(topic) unless exists_in_local
808
877
  end
809
878
 
810
879
  topics
@@ -816,16 +885,146 @@ module Howzit
816
885
  ## @param path [String] The build note path
817
886
  ##
818
887
  def read_help(path = nil)
819
- @topics = read_help_file(path)
820
- return unless Howzit.options[:include_upstream]
888
+ # Stack mode only applies when reading the main build note
889
+ # When reading templates or included files, use normal mode to prevent recursion
890
+
891
+ # Check if this is a template file
892
+ is_template = false
893
+ if path && Howzit.config.respond_to?(:template_folder) && Howzit.config.template_folder
894
+ template_folder = File.expand_path(Howzit.config.template_folder)
895
+ file_path = File.expand_path(path)
896
+ is_template = file_path.start_with?(template_folder)
897
+ end
898
+
899
+ # Prevent recursion: if we're already processing stack mode, skip it
900
+ in_stack_mode = Howzit.instance_variable_defined?(:@in_stack_mode) && Howzit.instance_variable_get(:@in_stack_mode)
901
+
902
+ # Use stack mode only if:
903
+ # 1. Stack option is enabled
904
+ # 2. We're not already in stack mode (prevent recursion)
905
+ # 3. We're not reading a template file
906
+ # 4. We're reading the main build note (no specific path provided, or path matches the main note file)
907
+ main_note = path.nil? || begin
908
+ main_file = note_file
909
+ main_file && File.expand_path(path) == File.expand_path(main_file)
910
+ rescue StandardError
911
+ false
912
+ end
913
+
914
+ use_stack = Howzit.options[:stack] && !in_stack_mode && !is_template && main_note
915
+
916
+ if use_stack
917
+ # Set flag to prevent recursive stack mode calls
918
+ Howzit.instance_variable_set(:@in_stack_mode, true)
919
+ begin
920
+ # Stack mode: read all build notes from current directory up to root
921
+ stack_files = glob_stack
922
+ @topics = []
923
+ existing_base_names = []
924
+
925
+ # Merge metadata from all stacked files
926
+ # Process from root to closest, with closer directories taking precedence
927
+ stacked_metadata = {}
928
+
929
+ # Read metadata from all files (root to closest)
930
+ # Process in reverse so closer files overwrite parent values
931
+ stack_files.reverse.each do |file|
932
+ file_content = Util.read_file(file)
933
+ next if file_content.nil? || file_content.empty?
934
+
935
+ file_meta = file_content.split(/^#/)[0].strip.metadata
936
+ # Merge metadata: closer directories overwrite parent directories
937
+ # Processing root to closest means closer files overwrite parent values
938
+ next unless file_meta.is_a?(Hash)
939
+
940
+ file_meta.each do |key, value|
941
+ stacked_metadata[key] = value
942
+ end
943
+ end
944
+
945
+ # Get global metadata
946
+ global_meta = {}
947
+ raw_global = Howzit.options[:metadata] || Howzit.options['metadata']
948
+ if raw_global.is_a?(Hash)
949
+ raw_global.each do |k, v|
950
+ global_meta[k.to_s.downcase] = v
951
+ end
952
+ end
953
+
954
+ # Get passed metadata (from meta argument in initialize)
955
+ # Store it before we rebuild @metadata
956
+ original_metadata = @metadata.dup if @metadata.is_a?(Hash)
821
957
 
822
- unless Howzit.has_read_upstream
823
- upstream_topics = read_upstream
958
+ # Merge order for final metadata (closest directory takes precedence):
959
+ # 1. Global config metadata (lowest precedence)
960
+ # 2. Stacked metadata from all directories (closer overwrites parent)
961
+ # 3. Metadata passed in via meta argument (e.g., templates) - highest precedence
962
+ final_metadata = global_meta.dup
963
+ final_metadata.merge!(stacked_metadata)
824
964
 
825
- upstream_topics.each do |topic|
826
- @topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
965
+ # Add back any passed metadata that was in the original @metadata
966
+ # These are keys that exist in original but not in global or stacked
967
+ original_metadata&.each do |key, value|
968
+ next if global_meta.key?(key) || stacked_metadata.key?(key)
969
+
970
+ final_metadata[key] = value
971
+ end
972
+
973
+ @metadata = final_metadata
974
+
975
+ # Set primary note file to the closest one (first in stack)
976
+ @note_file = stack_files.first if stack_files.any?
977
+
978
+ # Read topics from each file, closest first
979
+ stack_files.each do |file|
980
+ file_topics = read_help_file(file)
981
+ file_topics.each do |topic|
982
+ # Ensure topic has the correct source_file set (always absolute path)
983
+ abs_file = File.expand_path(file)
984
+ topic.instance_variable_set(:@source_file, abs_file) unless topic.source_file == abs_file
985
+ base_name = base_topic_name(topic.title)
986
+ # Only add if we haven't seen this base topic name yet
987
+ unless existing_base_names.include?(base_name)
988
+ @topics.push(topic)
989
+ existing_base_names.push(base_name)
990
+ end
991
+ end
992
+ end
993
+
994
+ # Load template topics from the main build note (closest file) only
995
+ # Templates should only be loaded once, from the main build note
996
+ if stack_files.any?
997
+ main_file = stack_files.first
998
+ main_content = Util.read_file(main_file)
999
+ if main_content
1000
+ main_template_topics = get_template_topics(main_content)
1001
+ main_template_topics.each do |topic|
1002
+ # Check if a topic with this base name already exists
1003
+ base_name = base_topic_name(topic.title)
1004
+ unless existing_base_names.include?(base_name)
1005
+ @topics.push(topic)
1006
+ existing_base_names.push(base_name)
1007
+ end
1008
+ end
1009
+ end
1010
+ end
1011
+ ensure
1012
+ # Clear the flag when done
1013
+ Howzit.instance_variable_set(:@in_stack_mode, false)
1014
+ end
1015
+ else
1016
+ # Normal mode: read single build note file
1017
+ @topics = read_help_file(path)
1018
+ return unless Howzit.options[:include_upstream]
1019
+
1020
+ unless Howzit.has_read_upstream
1021
+ upstream_topics = read_upstream
1022
+
1023
+ upstream_topics.each do |topic|
1024
+ @topics.push(topic) unless find_topic(topic.title.sub(/^.+:/, '')).count.positive?
1025
+ end
1026
+ Howzit.has_read_upstream = true
827
1027
  end
828
- Howzit.has_read_upstream = true
829
1028
  end
830
1029
 
831
1030
  return unless note_file && @topics.empty?
@@ -968,10 +1167,16 @@ module Howzit
968
1167
  when :all
969
1168
  topic_matches.concat(matches.sort_by(&:title))
970
1169
  else
971
- selected_titles = Prompt.choose(matches.map(&:title), height: :max, query: Howzit.options[:grep])
1170
+ # Format titles with abbreviated paths for menu display
1171
+ titles = matches.map { |topic| format_topic_title_for_menu(topic) }
1172
+ selected_titles = Prompt.choose(titles, height: :max, query: Howzit.options[:grep])
972
1173
  # Convert selected titles back to topic objects
973
1174
  selected_titles.each do |title|
974
- matched_topic = matches.find { |t| t.title == title }
1175
+ # Match by formatted title or base title
1176
+ matched_topic = matches.find do |t|
1177
+ formatted = format_topic_title_for_menu(t)
1178
+ formatted == title || t.title == title || t.title.sub(/^[^:]+:\s*/, '') == title.sub(/^[^:]+:\s*/, '')
1179
+ end
975
1180
  topic_matches.push(matched_topic) if matched_topic
976
1181
  end
977
1182
  end
@@ -1095,6 +1300,46 @@ module Howzit
1095
1300
  all_matches
1096
1301
  end
1097
1302
 
1303
+ ##
1304
+ ## Abbreviate a directory path for menu display
1305
+ ##
1306
+ ## @param source_file [String] Path to the source build note file
1307
+ ## @param current_dir [String] Current working directory (default: Dir.pwd)
1308
+ ##
1309
+ ## @return [String] Last directory name, or nil if same directory
1310
+ ##
1311
+ def abbreviate_path(source_file, current_dir = Dir.pwd)
1312
+ return nil unless source_file
1313
+
1314
+ source_dir = File.dirname(File.expand_path(source_file))
1315
+ current_dir = File.expand_path(current_dir)
1316
+
1317
+ # If same directory, return nil (no prefix needed)
1318
+ return nil if source_dir == current_dir
1319
+
1320
+ # Return just the last directory name
1321
+ File.basename(source_dir)
1322
+ end
1323
+
1324
+ ##
1325
+ ## Format topic title for menu display with abbreviated path
1326
+ ##
1327
+ ## @param topic [Topic] The topic to format
1328
+ ##
1329
+ ## @return [String] Formatted title with abbreviated path prefix
1330
+ ##
1331
+ def format_topic_title_for_menu(topic)
1332
+ return topic.title unless topic.source_file
1333
+
1334
+ abbrev = abbreviate_path(topic.source_file)
1335
+ return topic.title if abbrev.nil?
1336
+
1337
+ # Extract base title (remove existing path prefix if present)
1338
+ base_title = topic.title.sub(/^[^:]+:\s*/, '')
1339
+
1340
+ "#{abbrev}: #{base_title}"
1341
+ end
1342
+
1098
1343
  ##
1099
1344
  ## Resolve fuzzy matches for a search term
1100
1345
  ##
@@ -1116,11 +1361,16 @@ module Howzit
1116
1361
  when :all
1117
1362
  matches
1118
1363
  else
1119
- titles = matches.map(&:title)
1364
+ # Format titles with abbreviated paths for menu display
1365
+ titles = matches.map { |topic| format_topic_title_for_menu(topic) }
1120
1366
  res = Prompt.choose(titles, query: search_term)
1121
1367
  # Convert selected titles back to topic objects from the original matches
1122
1368
  res.flat_map do |title|
1123
- matched_topic = matches.find { |t| t.title == title }
1369
+ # Match by formatted title or base title
1370
+ matched_topic = matches.find do |t|
1371
+ formatted = format_topic_title_for_menu(t)
1372
+ formatted == title || t.title == title || t.title.sub(/^[^:]+:\s*/, '') == title.sub(/^[^:]+:\s*/, '')
1373
+ end
1124
1374
  matched_topic || find_topic(title)[0]
1125
1375
  end.compact
1126
1376
 
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