sakusei 0.2.0 → 0.3.0

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: 3c5167b69a1150bb93726449a31f46408381ae14f26c0034e472a3ce6b22ee3f
4
- data.tar.gz: b2187d77e9222b69d927da57ed085cb876e7e0aa7e2c2f556bf4bbfc9e8bbc1b
3
+ metadata.gz: ab634a886e4a04c4a309f672ff57317e54eab5d44e8fcac304a42c9463c5bb15
4
+ data.tar.gz: 4c2e2a610935627d01c80dd0f0b7ad48f6a840dddf2b433a122f4655f3a27826
5
5
  SHA512:
6
- metadata.gz: b15a0b12a135ce5eec0d4ab4afa9db615abac32405656456b15ac66d612a13fb8bcfaea564824be18d74a201a947f8ba4d21e5f40a591cf6c90c59589d8443f2
7
- data.tar.gz: c9c3dcfa53b2fd665a614414cae22502d830e618c3018b32b00fe314ea5b200d91569483e0c31e5c095862e0999c03ae67d4a8e148b56e2a151007bec8ec6c50
6
+ metadata.gz: caf4eb2b35c3de565416c7001f5c5fb4e79837f3ddc5cb5817154b2ea4eb6e6125ad818b71520337b6381b90b9d0fd2ecbae392fe8b1d7645e41038df9d457fe
7
+ data.tar.gz: 579ad95d4b5a86a3095f7c6138a57bc577863cfecef7244520404bfddcd84281b9d405fbbc37053dcaf7dea6b7f8d3b81461325e186b7ba806fe547b684f96ff
data/lib/sakusei/cli.rb CHANGED
@@ -83,7 +83,8 @@ module Sakusei
83
83
  else
84
84
  say 'Available style packs:', :green
85
85
  style_packs.each do |pack|
86
- say " • #{pack[:name]}"
86
+ label = pack[:default] ? " (default)" : ''
87
+ say " • #{pack[:name]}#{label}"
87
88
  say " Path: #{pack[:path]}", :cyan
88
89
  end
89
90
  end
@@ -92,6 +93,16 @@ module Sakusei
92
93
  exit 1
93
94
  end
94
95
 
96
+ desc 'set-style NAME', 'Set the default style pack'
97
+ option :directory, aliases: '-d', default: '.', desc: 'Directory to search for .sakusei'
98
+ def set_style(name)
99
+ StylePack.set_default(options[:directory], name)
100
+ say "Default style pack set to '#{name}'", :green
101
+ rescue Error => e
102
+ say_error e.message
103
+ exit 1
104
+ end
105
+
95
106
  desc 'components [STYLE]', 'List available Vue components'
96
107
  option :directory, aliases: '-d', default: '.', desc: 'Directory to search for style packs'
97
108
  def components(style = nil)
@@ -10,15 +10,65 @@ module Sakusei
10
10
  # Raw HTML in the markdown (html: true) passes through md-to-pdf untouched.
11
11
  class HeadingWrapper
12
12
  HEADING_PATTERN = /\A[ \t]*(##|###) /
13
+ FENCE_PATTERN = /\A`{3,}/
13
14
 
14
15
  def initialize(content)
15
16
  @content = content
16
17
  end
17
18
 
18
19
  def wrap
20
+ code_blocks = []
21
+ text_lines = []
22
+ in_code = false
23
+ code_buffer = []
24
+
25
+ @content.each_line do |line|
26
+ if line.match?(FENCE_PATTERN)
27
+ if in_code
28
+ # End of code block
29
+ code_buffer << line
30
+ code_blocks << code_buffer.join
31
+ text_lines << "#{placeholder_for(code_blocks.length - 1)}\n\n"
32
+ code_buffer = []
33
+ in_code = false
34
+ else
35
+ # Start of code block
36
+ in_code = true
37
+ code_buffer << line
38
+ end
39
+ elsif in_code
40
+ code_buffer << line
41
+ else
42
+ text_lines << line
43
+ end
44
+ end
45
+
46
+ # Handle unclosed code block
47
+ if in_code
48
+ code_blocks << code_buffer.join
49
+ text_lines << "#{placeholder_for(code_blocks.length - 1)}\n\n"
50
+ end
51
+
52
+ wrapped = wrap_text(text_lines.join)
53
+
54
+ # Restore code blocks
55
+ code_blocks.each_with_index do |block, i|
56
+ wrapped = wrapped.sub(placeholder_for(i), block)
57
+ end
58
+
59
+ wrapped
60
+ end
61
+
62
+ private
63
+
64
+ def placeholder_for(index)
65
+ "__SAKUSEI_CODE_BLOCK_#{index}__"
66
+ end
67
+
68
+ def wrap_text(content)
19
69
  # Split on two-or-more blank lines to get top-level blocks.
20
70
  # Preserve the separator length so rejoining is faithful.
21
- blocks = @content.split(/(\n{2,})/)
71
+ blocks = content.split(/(\n{2,})/)
22
72
  # split with a capture group gives us [block, sep, block, sep, ...]
23
73
  result = []
24
74
  i = 0
@@ -47,8 +97,6 @@ module Sakusei
47
97
  result.join
48
98
  end
49
99
 
50
- private
51
-
52
100
  def heading_block?(block)
53
101
  return false unless block
54
102
  block.match?(HEADING_PATTERN)
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
+ require 'yaml'
4
5
 
5
6
  module Sakusei
6
7
  class StylePack
7
8
  STYLE_PACKS_DIR = 'style_packs'
8
9
  SAKUSEI_DIR = '.sakusei'
10
+ SAKUSEI_CONFIG = 'config.yml'
9
11
 
10
12
  attr_reader :name, :path, :config, :stylesheet, :header, :footer
11
13
 
@@ -24,16 +26,46 @@ module Sakusei
24
26
  def self.discover(start_dir, requested_name = nil)
25
27
  sakusei_path = find_sakusei_dir(start_dir)
26
28
 
27
- if sakusei_path
28
- packs_dir = File.join(sakusei_path, STYLE_PACKS_DIR)
29
- return load_from_path(packs_dir, requested_name) if Dir.exist?(packs_dir)
29
+ # Resolve name: explicit arg takes priority, then config default
30
+ resolved_name = requested_name
31
+ resolved_name ||= read_config(sakusei_path)['default_style'] if sakusei_path
32
+
33
+ if resolved_name
34
+ # Built-in 'default' pack requested explicitly — skip to fallback below
35
+ unless resolved_name == 'default'
36
+ # Search across ALL .sakusei dirs in the tree (not just the nearest),
37
+ # so a named pack in a parent dir is found even if a child .sakusei
38
+ # directory exists but has no style_packs/ subdirectory.
39
+ pack_entry = list_available(start_dir).find { |p| p[:name] == resolved_name }
40
+ raise Error, "Style pack '#{resolved_name}' not found. Run 'sakusei styles' to see available packs." unless pack_entry
41
+ return new(pack_entry[:path], pack_entry[:name])
42
+ end
43
+ else
44
+ # No name at all: use the first pack from the nearest .sakusei
45
+ if sakusei_path
46
+ packs_dir = File.join(sakusei_path, STYLE_PACKS_DIR)
47
+ return load_from_path(packs_dir, nil) if Dir.exist?(packs_dir)
48
+ end
30
49
  end
31
50
 
32
- # Fall back to default style pack
51
+ # Fall back to built-in default style pack
33
52
  default_path = File.expand_path('../templates/default_style_pack', __dir__)
34
53
  new(default_path, 'default')
35
54
  end
36
55
 
56
+ # Set the default style pack in the nearest .sakusei/config.yml
57
+ def self.set_default(start_dir, style_name)
58
+ sakusei_path = find_sakusei_dir(start_dir)
59
+ raise Error, "No .sakusei directory found. Run 'sakusei init' to create a style pack first." unless sakusei_path
60
+
61
+ available = list_available(start_dir)
62
+ unless available.any? { |p| p[:name] == style_name }
63
+ raise Error, "Style pack '#{style_name}' not found. Run 'sakusei styles' to see available packs."
64
+ end
65
+
66
+ write_config(sakusei_path, 'default_style' => style_name)
67
+ end
68
+
37
69
  # Initialize a new style pack
38
70
  def self.init(directory, name)
39
71
  sakusei_path = File.join(directory, SAKUSEI_DIR)
@@ -204,6 +236,7 @@ module Sakusei
204
236
  # List all available style packs
205
237
  def self.list_available(start_dir = '.')
206
238
  packs = []
239
+ nearest_sakusei_path = nil
207
240
 
208
241
  # Find all .sakusei directories walking up from start_dir
209
242
  current = File.expand_path(start_dir)
@@ -212,6 +245,7 @@ module Sakusei
212
245
  loop do
213
246
  sakusei_path = File.join(current, SAKUSEI_DIR)
214
247
  if Dir.exist?(sakusei_path) && !visited_dirs.include?(sakusei_path)
248
+ nearest_sakusei_path ||= sakusei_path
215
249
  visited_dirs.add(sakusei_path)
216
250
  packs_dir = File.join(sakusei_path, STYLE_PACKS_DIR)
217
251
  if Dir.exist?(packs_dir)
@@ -233,11 +267,29 @@ module Sakusei
233
267
 
234
268
  # Remove duplicates by name (closer packs take precedence)
235
269
  seen_names = Set.new
236
- packs.select { |p| seen_names.add?(p[:name]) }
270
+ unique_packs = packs.select { |p| seen_names.add?(p[:name]) }
271
+
272
+ # Mark whichever pack is set as default in config
273
+ default_name = nearest_sakusei_path ? read_config(nearest_sakusei_path)['default_style'] : nil
274
+ unique_packs.map { |p| p.merge(default: p[:name] == default_name) }
237
275
  end
238
276
 
239
277
  private
240
278
 
279
+ def self.read_config(sakusei_dir)
280
+ config_path = File.join(sakusei_dir, SAKUSEI_CONFIG)
281
+ return {} unless File.exist?(config_path)
282
+ YAML.safe_load(File.read(config_path)) || {}
283
+ rescue Psych::Exception
284
+ {}
285
+ end
286
+
287
+ def self.write_config(sakusei_dir, data)
288
+ config_path = File.join(sakusei_dir, SAKUSEI_CONFIG)
289
+ existing = read_config(sakusei_dir)
290
+ File.write(config_path, YAML.dump(existing.merge(data)))
291
+ end
292
+
241
293
  def self.find_sakusei_dir(start_dir)
242
294
  current = File.expand_path(start_dir)
243
295
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sakusei
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sakusei
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Rowell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-04-14 00:00:00.000000000 Z
11
+ date: 2026-04-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: thor