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 +4 -4
- data/CHANGELOG.md +69 -0
- data/README.md +4 -0
- data/bin/howzit +4 -0
- data/lib/howzit/buildnote.rb +266 -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/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: 3c0d8cd59db6974b2ceb826ab199eb7eab680919de813f2b2fe4f205c5506697
|
|
4
|
+
data.tar.gz: 1aa7e1e3d80fb27efa4e38d56e5d31d0643871418a80a1417ae045839e6989c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/howzit/buildnote.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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?)
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
820
|
-
|
|
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
|
-
|
|
823
|
-
|
|
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
|
-
|
|
826
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|