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 +4 -4
- data/CHANGELOG.md +84 -0
- data/README.md +4 -0
- data/bin/howzit +4 -0
- data/lib/howzit/buildnote.rb +271 -16
- data/lib/howzit/config.rb +28 -3
- data/lib/howzit/directive.rb +3 -0
- data/lib/howzit/script_support.rb +151 -49
- data/lib/howzit/stringutils.rb +20 -1
- data/lib/howzit/task.rb +80 -2
- data/lib/howzit/topic.rb +7 -5
- data/lib/howzit/version.rb +1 -1
- data/lib/howzit.rb +7 -1
- data/spec/cli_spec.rb +14 -2
- data/spec/conditional_blocks_integration_spec.rb +8 -7
- data/spec/log_level_spec.rb +10 -9
- data/spec/sequential_conditional_spec.rb +9 -8
- data/spec/set_var_spec.rb +26 -25
- data/spec/spec_helper.rb +1 -0
- data/spec/stack_mode_spec.rb +321 -0
- data/src/_README.md +4 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a934719a022ca2f6713474e797eb1a9662dc884a750c93648d4151506ae41571
|
|
4
|
+
data.tar.gz: f261e5e733bd2d695cc76ce36e74096d357983301156c6b5b2ce5757aa3637eb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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?)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
820
|
-
|
|
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
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
125
|
+
return false unless @ignore_patterns.is_a?(Array)
|
|
101
126
|
|
|
102
127
|
ignore = false
|
|
103
128
|
|
data/lib/howzit/directive.rb
CHANGED
|
@@ -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
|