yard-yaml 0.1.0 → 0.1.2

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.
data/certs/pboling.pem ADDED
@@ -0,0 +1,27 @@
1
+ -----BEGIN CERTIFICATE-----
2
+ MIIEgDCCAuigAwIBAgIBATANBgkqhkiG9w0BAQsFADBDMRUwEwYDVQQDDAxwZXRl
3
+ ci5ib2xpbmcxFTATBgoJkiaJk/IsZAEZFgVnbWFpbDETMBEGCgmSJomT8ixkARkW
4
+ A2NvbTAeFw0yNTA1MDQxNTMzMDlaFw00NTA0MjkxNTMzMDlaMEMxFTATBgNVBAMM
5
+ DHBldGVyLmJvbGluZzEVMBMGCgmSJomT8ixkARkWBWdtYWlsMRMwEQYKCZImiZPy
6
+ LGQBGRYDY29tMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAruUoo0WA
7
+ uoNuq6puKWYeRYiZekz/nsDeK5x/0IEirzcCEvaHr3Bmz7rjo1I6On3gGKmiZs61
8
+ LRmQ3oxy77ydmkGTXBjruJB+pQEn7UfLSgQ0xa1/X3kdBZt6RmabFlBxnHkoaGY5
9
+ mZuZ5+Z7walmv6sFD9ajhzj+oIgwWfnEHkXYTR8I6VLN7MRRKGMPoZ/yvOmxb2DN
10
+ coEEHWKO9CvgYpW7asIihl/9GMpKiRkcYPm9dGQzZc6uTwom1COfW0+ZOFrDVBuV
11
+ FMQRPswZcY4Wlq0uEBLPU7hxnCL9nKK6Y9IhdDcz1mY6HZ91WImNslOSI0S8hRpj
12
+ yGOWxQIhBT3fqCBlRIqFQBudrnD9jSNpSGsFvbEijd5ns7Z9ZMehXkXDycpGAUj1
13
+ to/5cuTWWw1JqUWrKJYoifnVhtE1o1DZ+LkPtWxHtz5kjDG/zR3MG0Ula0UOavlD
14
+ qbnbcXPBnwXtTFeZ3C+yrWpE4pGnl3yGkZj9SMTlo9qnTMiPmuWKQDatAgMBAAGj
15
+ fzB9MAkGA1UdEwQCMAAwCwYDVR0PBAQDAgSwMB0GA1UdDgQWBBQE8uWvNbPVNRXZ
16
+ HlgPbc2PCzC4bjAhBgNVHREEGjAYgRZwZXRlci5ib2xpbmdAZ21haWwuY29tMCEG
17
+ A1UdEgQaMBiBFnBldGVyLmJvbGluZ0BnbWFpbC5jb20wDQYJKoZIhvcNAQELBQAD
18
+ ggGBAJbnUwfJQFPkBgH9cL7hoBfRtmWiCvdqdjeTmi04u8zVNCUox0A4gT982DE9
19
+ wmuN12LpdajxZONqbXuzZvc+nb0StFwmFYZG6iDwaf4BPywm2e/Vmq0YG45vZXGR
20
+ L8yMDSK1cQXjmA+ZBKOHKWavxP6Vp7lWvjAhz8RFwqF9GuNIdhv9NpnCAWcMZtpm
21
+ GUPyIWw/Cw/2wZp74QzZj6Npx+LdXoLTF1HMSJXZ7/pkxLCsB8m4EFVdb/IrW/0k
22
+ kNSfjtAfBHO8nLGuqQZVH9IBD1i9K6aSs7pT6TW8itXUIlkIUI2tg5YzW6OFfPzq
23
+ QekSkX3lZfY+HTSp/o+YvKkqWLUV7PQ7xh1ZYDtocpaHwgxe/j3bBqHE+CUPH2vA
24
+ 0V/FwdTRWcwsjVoOJTrYcff8pBZ8r2MvtAc54xfnnhGFzeRHfcltobgFxkAXdE6p
25
+ DVjBtqT23eugOqQ73umLcYDZkc36vnqGxUBSsXrzY9pzV5gGr2I8YUxMqf6ATrZt
26
+ L9nRqA==
27
+ -----END CERTIFICATE-----
@@ -12,10 +12,17 @@ module Yard
12
12
  #
13
13
  # Note: We intentionally keep the contract minimal and stable. Tests stub the backend.
14
14
  class Converter
15
+ BACKEND_STATE = {backend: nil}
16
+ BACKEND_MUTEX = Mutex.new
17
+
15
18
  class << self
16
19
  # Assignable backend for dependency injection in tests.
17
- # Expected to respond to `convert(yaml, options)` and return a Hash with :html, :title, :description, and :meta keys
18
- attr_writer :backend
20
+ # Preferred backends respond to `to_markdown(yaml, options:)`. Legacy
21
+ # test backends may respond to `convert(yaml, options)` and return a
22
+ # Hash with :html, :title, :description, and :meta keys.
23
+ def backend=(backend)
24
+ BACKEND_MUTEX.synchronize { BACKEND_STATE[:backend] = backend }
25
+ end
19
26
 
20
27
  # Convert a YAML string into an HTML result.
21
28
  #
@@ -34,49 +41,119 @@ module Yard
34
41
  # @param config [Yard::Yaml::Config]
35
42
  # @return [Hash]
36
43
  def from_file(path, options = {}, config: Yard::Yaml.config)
37
- content = read_file(path)
44
+ content = read_file(path, config: config)
38
45
  return empty_result if content.nil?
39
46
  run_convert(content, options.merge(source_path: path.to_s), config)
40
47
  end
41
48
 
42
49
  # Backend accessor with auto-discovery.
43
50
  def backend
44
- return @backend if defined?(@backend) && @backend
51
+ configured_backend = BACKEND_MUTEX.synchronize { BACKEND_STATE[:backend] }
52
+ return configured_backend if configured_backend
53
+
45
54
  begin
46
55
  require "yaml/converter"
47
56
  rescue LoadError
48
57
  # ignore; backend may be set by tests
49
58
  end
50
- @backend = if defined?(::Yaml) && ::Yaml.const_defined?(:Converter)
51
- ::Yaml::Converter
59
+
60
+ BACKEND_MUTEX.synchronize do
61
+ BACKEND_STATE[:backend] ||= ::Yaml::Converter if defined?(::Yaml) && ::Yaml.const_defined?(:Converter)
52
62
  end
53
63
  end
54
64
 
55
65
  private
56
66
 
57
- def read_file(path)
58
- File.read(path.to_s)
67
+ def read_file(path, config: Yard::Yaml.config)
68
+ raw = File.binread(path.to_s)
69
+ if binary_content?(raw)
70
+ handle_error(Yard::Yaml::Error.new("binary file not supported"), strict: config.strict, context: path.to_s)
71
+ return
72
+ end
73
+
74
+ content = raw.dup.force_encoding(Encoding::UTF_8)
75
+ return content if content.valid_encoding?
76
+
77
+ if config.strict
78
+ handle_error(Yard::Yaml::Error.new("invalid UTF-8 bytes in file"), strict: true, context: path.to_s)
79
+ return
80
+ end
81
+
82
+ handle_error(
83
+ Yard::Yaml::Error.new("invalid UTF-8 bytes in file; replaced invalid sequences"),
84
+ strict: false,
85
+ context: path.to_s,
86
+ )
87
+ content.scrub
59
88
  rescue Errno::ENOENT => e
60
- handle_error(e, strict: Yard::Yaml.config.strict, context: "missing file: #{path}")
89
+ handle_error(e, strict: config.strict, context: "missing file: #{path}")
61
90
  nil
62
91
  end
63
92
 
93
+ def binary_content?(raw)
94
+ sample = raw.byteslice(0, 4096) || "".b
95
+ return true if sample.include?("\x00")
96
+ return false if sample.empty?
97
+
98
+ control_bytes = sample.bytes.count do |byte|
99
+ (0..8).cover?(byte) || byte == 11 || byte == 12 || (14..31).cover?(byte)
100
+ end
101
+ control_bytes.fdiv(sample.bytesize) > 0.1
102
+ end
103
+
64
104
  def run_convert(yaml, options, config)
65
105
  opts = build_options(options, config)
66
106
  b = backend
67
- unless b && b.respond_to?(:convert)
107
+ unless converter_backend?(b)
68
108
  handle_error(Yard::Yaml::Error.new("yaml-converter backend not available"), strict: config.strict, context: "backend")
69
109
  return empty_result
70
110
  end
71
111
 
72
112
  begin
73
- normalize_result(b.convert(yaml, opts))
113
+ normalize_result(convert_with_backend(b, yaml, opts))
74
114
  rescue StandardError => e
75
115
  handle_error(e, strict: config.strict, context: opts[:source_path] || "string")
76
116
  empty_result
77
117
  end
78
118
  end
79
119
 
120
+ def converter_backend?(backend)
121
+ backend&.respond_to?(:to_markdown) || backend&.respond_to?(:convert)
122
+ end
123
+
124
+ def convert_with_backend(backend, yaml, options)
125
+ return convert_markdown_backend(backend, yaml, options) if backend.respond_to?(:to_markdown)
126
+
127
+ backend.convert(yaml, options)
128
+ end
129
+
130
+ def convert_markdown_backend(backend, yaml, options)
131
+ markdown = backend.to_markdown(yaml, options: options)
132
+ metadata = metadata_from_yaml(yaml)
133
+ {
134
+ html: markdown_to_html(markdown),
135
+ title: metadata["title"],
136
+ description: metadata["abstract"],
137
+ meta: metadata,
138
+ }
139
+ end
140
+
141
+ def markdown_to_html(markdown)
142
+ require "kramdown"
143
+ require "kramdown-parser-gfm"
144
+
145
+ Kramdown::Document.new(markdown.to_s, input: "GFM").to_html
146
+ end
147
+
148
+ def metadata_from_yaml(yaml)
149
+ require "yaml"
150
+
151
+ parsed = YAML.safe_load(yaml.to_s, permitted_classes: [], permitted_symbols: [], aliases: false)
152
+ parsed.is_a?(Hash) ? parsed : {}
153
+ rescue Psych::Exception
154
+ {}
155
+ end
156
+
80
157
  def build_options(options, config)
81
158
  safe = {
82
159
  allow_erb: !!config.allow_erb,
@@ -25,13 +25,14 @@ module Yard
25
25
  # @return [Array<String>] list of written file paths
26
26
  def emit!(pages:, output_dir:, config: Yard::Yaml.config)
27
27
  pages = Array(pages)
28
+ pages_with_slugs = assign_slugs(pages)
28
29
  written = []
29
30
  base = File.join(output_dir.to_s, config.out_dir.to_s)
30
31
  FileUtils.mkdir_p(base)
31
32
 
32
33
  # Write per-page files
33
- pages.each do |page|
34
- slug = page_slug(page)
34
+ pages_with_slugs.each do |page|
35
+ slug = page.fetch(:__yard_yaml_slug)
35
36
  path = File.join(base, "#{slug}.html")
36
37
  html = render_page_html(page)
37
38
  atomic_write(path, html, strict: config.strict)
@@ -41,7 +42,7 @@ module Yard
41
42
  # Index (optional)
42
43
  if config.index
43
44
  index_path = File.join(base, "index.html")
44
- html = render_index_html(pages)
45
+ html = render_index_html(pages_with_slugs)
45
46
  atomic_write(index_path, html, strict: config.strict)
46
47
  written << index_path
47
48
  end
@@ -58,20 +59,36 @@ module Yard
58
59
 
59
60
  private
60
61
 
62
+ def assign_slugs(pages)
63
+ seen = Hash.new(0)
64
+ pages.map do |page|
65
+ slug = page_slug(page)
66
+ count = seen[slug]
67
+ seen[slug] += 1
68
+ page.merge(__yard_yaml_slug: count.zero? ? slug : "#{slug}-#{count + 1}")
69
+ end
70
+ end
71
+
61
72
  def page_slug(page)
62
73
  meta = page[:meta] || {}
63
74
  slug = meta["slug"] || meta[:slug]
64
75
  return sanitize_slug(slug) if slug && !slug.to_s.empty?
65
76
 
66
- title = page[:title].to_s
67
- return sanitize_slug(title) unless title.empty?
68
-
69
77
  if page[:path]
70
- base = File.basename(page[:path].to_s, File.extname(page[:path].to_s))
71
- return sanitize_slug(base)
78
+ path_slug = path_slug(page[:path])
79
+ return path_slug unless path_slug.empty?
72
80
  end
73
81
 
74
- "page"
82
+ title = sanitize_slug(page[:title])
83
+ title.empty? ? "page" : title
84
+ end
85
+
86
+ def path_slug(path)
87
+ relative = path.to_s.delete_prefix("#{Dir.pwd}/")
88
+ dirname = File.dirname(relative)
89
+ basename = File.basename(relative, File.extname(relative))
90
+ parts = (dirname == ".") ? [basename] : dirname.split(File::SEPARATOR) + [basename]
91
+ sanitize_slug(parts.join("-"))
75
92
  end
76
93
 
77
94
  def sanitize_slug(s)
@@ -108,8 +125,8 @@ module Yard
108
125
 
109
126
  def render_index_html(pages)
110
127
  rows = pages.map do |p|
111
- title = p[:title] || page_slug(p)
112
- slug = page_slug(p)
128
+ slug = p[:__yard_yaml_slug] || page_slug(p)
129
+ title = p[:title] || slug
113
130
  desc = p[:description]
114
131
  %(<li><a href="#{escape_html(slug)}.html">#{escape_html(title)}</a>#{" — #{escape_html(desc)}" if desc}</li>)
115
132
  end.join("\n")
@@ -4,7 +4,8 @@ module Yard
4
4
  module Yaml
5
5
  # Plugin activation for yard-yaml (Phase 3: config + discovery; still no YARD registrations).
6
6
  module Plugin
7
- @activated = false
7
+ STATE = {activated: false, at_exit_installed: false}
8
+ STATE_MUTEX = Mutex.new
8
9
 
9
10
  class << self
10
11
  # Whether the plugin has been activated for the current process.
@@ -12,7 +13,7 @@ module Yard
12
13
  #
13
14
  # @return [Boolean]
14
15
  def activated?
15
- @activated
16
+ STATE[:activated]
16
17
  end
17
18
 
18
19
  # Activate the plugin.
@@ -44,16 +45,82 @@ module Yard
44
45
  # Non-strict errors are already warned by converter/discovery
45
46
  end
46
47
 
47
- @activated = true
48
+ STATE_MUTEX.synchronize { STATE[:activated] = true }
48
49
  nil
49
50
  end
50
51
 
52
+ # Install an at-exit emitter for YARD's plugin loader. YARD loads
53
+ # plugins before it has generated the HTML tree, so converted YAML
54
+ # pages must be written after YARD finishes.
55
+ #
56
+ # @param argv [Array<String>, nil] the YARD argv used to discover output
57
+ # @return [void]
58
+ def install_at_exit(argv = nil)
59
+ should_install = STATE_MUTEX.synchronize do
60
+ if STATE[:at_exit_installed]
61
+ false
62
+ else
63
+ STATE[:at_exit_installed] = true
64
+ end
65
+ end
66
+ return unless should_install
67
+
68
+ at_exit do
69
+ emit!(output_dir: yard_output_dir(argv || ARGV))
70
+ end
71
+ nil
72
+ end
73
+
74
+ # Emit converted pages collected during activation.
75
+ #
76
+ # @param output_dir [String] YARD HTML output directory
77
+ # @return [Array<String>]
78
+ def emit!(output_dir:)
79
+ pages = Yard::Yaml.pages
80
+ return [] if pages.nil? || pages.empty?
81
+
82
+ Yard::Yaml::Emitter.emit!(pages: pages, output_dir: output_dir, config: Yard::Yaml.config)
83
+ end
84
+
85
+ # Resolve YARD's HTML output directory from argv or .yardopts.
86
+ #
87
+ # @param argv [Array<String>]
88
+ # @return [String]
89
+ def yard_output_dir(argv)
90
+ output_dir_from_tokens(Array(argv).map(&:to_s)) ||
91
+ output_dir_from_tokens(yardopts_tokens) ||
92
+ "doc"
93
+ end
94
+
51
95
  # Test-helper: reset internal activation flag.
52
96
  # Not part of public API; used from test teardown to avoid state leakage.
53
97
  def __reset_state__
54
- @activated = false
98
+ STATE_MUTEX.synchronize do
99
+ STATE[:activated] = false
100
+ STATE[:at_exit_installed] = false
101
+ end
102
+ nil
103
+ end
104
+
105
+ private
106
+
107
+ def output_dir_from_tokens(tokens)
108
+ tokens.each_with_index do |token, index|
109
+ return tokens[index + 1] if token == "--output" || token == "-o"
110
+ match = token.match(/\A--output=(.+)\z/)
111
+ return match[1] if match
112
+ end
55
113
  nil
56
114
  end
115
+
116
+ def yardopts_tokens
117
+ return [] unless File.file?(".yardopts")
118
+
119
+ require "shellwords"
120
+ Shellwords.split(File.read(".yardopts"))
121
+ rescue StandardError
122
+ []
123
+ end
57
124
  end
58
125
  end
59
126
  end
@@ -19,7 +19,7 @@ module Yard
19
19
  # @param config [Yard::Yaml::Config]
20
20
  # @return [String] HTML fragment (may be empty string)
21
21
  def render_for(object, base_dir: Dir.pwd, config: Yard::Yaml.config)
22
- return "" unless object && object.respond_to?(:tags)
22
+ return "" unless object&.respond_to?(:tags)
23
23
 
24
24
  parts = []
25
25
 
@@ -24,14 +24,10 @@ module Yard
24
24
  end
25
25
  else
26
26
  # Create a minimal shim that records define_tag calls
27
+ calls = []
27
28
  lib = Module.new
28
- class << lib
29
- attr_accessor :calls
30
- def define_tag(*args)
31
- self.calls ||= []
32
- self.calls << args
33
- end
34
- end
29
+ lib.define_singleton_method(:calls) { calls }
30
+ lib.define_singleton_method(:define_tag) { |*args| calls << args }
35
31
  begin
36
32
  ::YARD::Tags.const_set(:Library, lib)
37
33
  rescue StandardError
@@ -3,8 +3,8 @@
3
3
  module Yard
4
4
  module Yaml
5
5
  module Version
6
- VERSION = "0.1.0"
6
+ VERSION = "0.1.2"
7
7
  end
8
- VERSION = Version::VERSION # Support the traditional VERSION constant.
8
+ VERSION = Version::VERSION # Traditional Constant Location
9
9
  end
10
10
  end
data/lib/yard/yaml.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "version_gem"
3
4
  require_relative "yaml/version"
5
+
6
+ Yard::Yaml::Version.class_eval do
7
+ extend VersionGem::Basic
8
+ end
4
9
  require_relative "yaml/config"
5
10
  require_relative "yaml/cli"
6
11
  require_relative "yaml/plugin"
@@ -17,8 +22,11 @@ module Yard
17
22
  # Generic error for yard-yaml
18
23
  class Error < StandardError; end
19
24
 
20
- @config = nil
21
- @pages = nil
25
+ STATE = {
26
+ config: nil,
27
+ pages: nil,
28
+ }
29
+ STATE_MUTEX = Mutex.new
22
30
 
23
31
  class << self
24
32
  # Access the global configuration for yard-yaml.
@@ -28,19 +36,21 @@ module Yard
28
36
  #
29
37
  # @return [Yard::Yaml::Config]
30
38
  def config
31
- @config ||= Config.new
39
+ STATE[:config] || STATE_MUTEX.synchronize { STATE[:config] ||= Config.new }
32
40
  end
33
41
 
34
42
  # Access collected pages (Phase 3). Nil until plugin activation performs discovery.
35
43
  # Each page is a Hash with keys: :path, :html, :title, :description, :meta
36
44
  # @return [Array<Hash>, nil]
37
- attr_reader :pages
45
+ def pages
46
+ STATE[:pages]
47
+ end
38
48
 
39
49
  # Internal: set collected pages (used by Plugin during activation)
40
50
  def __set_pages__(list)
41
- @pages = Array(list)
42
- mirror_pages_to_registry(@pages)
43
- @pages
51
+ pages = STATE_MUTEX.synchronize { STATE[:pages] = Array(list) }
52
+ mirror_pages_to_registry(pages)
53
+ pages
44
54
  end
45
55
 
46
56
  # Configure the plugin programmatically.
@@ -80,8 +90,10 @@ module Yard
80
90
 
81
91
  # Test-helper: reset memoized config to defaults (not public API)
82
92
  def __reset_state__
83
- @config = nil
84
- @pages = nil
93
+ STATE_MUTEX.synchronize do
94
+ STATE[:config] = nil
95
+ STATE[:pages] = nil
96
+ end
85
97
  if defined?(::Yard::Yaml::Plugin) && ::Yard::Yaml::Plugin.respond_to?(:__reset_state__)
86
98
  ::Yard::Yaml::Plugin.__reset_state__
87
99
  end
data/lib/yard-yaml.rb CHANGED
@@ -4,3 +4,6 @@
4
4
  # YARD tries requiring several patterns; providing `yard-yaml` ensures
5
5
  # it can be loaded regardless of whether YARD attempts `yard-yaml` or `yard/yaml`.
6
6
  require_relative "yard/yaml"
7
+
8
+ Yard::Yaml::Plugin.activate(ARGV)
9
+ Yard::Yaml::Plugin.install_at_exit(ARGV)
@@ -0,0 +1,8 @@
1
+ module Yard
2
+ module Yaml
3
+ module Version
4
+ VERSION: String
5
+ end
6
+ VERSION: String
7
+ end
8
+ end
data.tar.gz.sig CHANGED
Binary file