asciisourcerer 0.3.1 → 0.5.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.
@@ -16,6 +16,23 @@ module Sourcerer
16
16
  # raw text (i.e., were not resolved by the parser).
17
17
  INCLUDE_DIRECTIVE_PATTERN = /include::[^\[]+\[[^\]]*\]/
18
18
 
19
+ # Matches a real (non-code-block) include directive at the start of a line,
20
+ # capturing the target path and its attribute list.
21
+ INCLUDE_LINE_PATTERN = /\A\s*include::([^\[]+)\[([^\]]*)\]\s*\z/
22
+
23
+ # Toggled by classic listing (----) / literal (....) block delimiters, so
24
+ # #detect_general_includes can skip include directives already captured
25
+ # (as literal example text) by the code_blocks/literal_blocks categories.
26
+ FENCE_DELIMITER_PATTERN = /\A(?:-{4,}|\.{4,})\s*\z/
27
+
28
+ # A document with a real title line can still end up with no `title`
29
+ # document attribute if a preceding, unresolved include directive
30
+ # disrupts Asciidoctor's header parsing (a known Asciidoctor quirk, not
31
+ # something this document did wrong). When that happens, Document#doctitle
32
+ # silently falls back to the first section's title. Detect that specific
33
+ # failure mode and recover the real title from the raw source instead.
34
+ RAW_TITLE_LINE_PATTERN = /\A=\s+(\S.*)\z/
35
+
19
36
  def process document, config: Config.new
20
37
  @config = config
21
38
  @main_file = document.attr('docfile')
@@ -36,7 +53,7 @@ module Sourcerer
36
53
  assign_line_ends(tree, doc_end)
37
54
 
38
55
  result = {
39
- title: document.doctitle,
56
+ title: doctitle_for(document),
40
57
  lines: doc_end
41
58
  }
42
59
 
@@ -61,12 +78,88 @@ module Sourcerer
61
78
  result[:admonitions] = @admonitions if @config.include?(:admonitions)
62
79
  result[:quotes] = @quotes if @config.include?(:quotes)
63
80
  result[:images] = @images if @config.include?(:images)
81
+ result[:includes] = detect_general_includes(document) if @config.include?(:includes)
64
82
 
65
83
  result
66
84
  end
67
85
 
68
86
  private
69
87
 
88
+ # Prefer Asciidoctor's own doctitle, but recover from the header-parsing
89
+ # failure mode described at RAW_TITLE_LINE_PATTERN: if the `title`
90
+ # attribute never got set (the real signal that header parsing broke,
91
+ # as opposed to a document that's genuinely untitled), look for an
92
+ # explicit `= Title` line in the raw source before trusting the
93
+ # first-section fallback.
94
+ def doctitle_for document
95
+ return document.doctitle if document.attr('title')
96
+
97
+ raw_title = raw_doctitle(document)
98
+ raw_title || document.doctitle
99
+ end
100
+
101
+ def raw_doctitle document
102
+ lines = document.source_lines
103
+ return nil unless lines
104
+
105
+ lines.each do |line|
106
+ return ::Regexp.last_match(1) if line =~ RAW_TITLE_LINE_PATTERN
107
+ # A section heading appearing before any `=` title line means the
108
+ # document genuinely has no title; stop looking.
109
+ break if line =~ /\A==+\s+\S/
110
+ end
111
+ nil
112
+ end
113
+
114
+ # Scan the raw source (outside of listing/literal delimited blocks, which
115
+ # are already covered by the code_blocks/literal_blocks `includes` field)
116
+ # for include directives, whether or not they were resolved by the parser.
117
+ def detect_general_includes document
118
+ lines = document.source_lines
119
+ return [] unless lines
120
+
121
+ offset = source_line_offset(document)
122
+ includes = []
123
+ in_fence = false
124
+ lines.each_with_index do |line, idx|
125
+ if line =~ FENCE_DELIMITER_PATTERN
126
+ in_fence = !in_fence
127
+ next
128
+ end
129
+ next if in_fence
130
+
131
+ next unless (m = line.match(INCLUDE_LINE_PATTERN))
132
+
133
+ entry = { target: m[1].strip, starts_at: idx + 1 + offset }
134
+ entry.merge!(parse_include_attrs(m[2]))
135
+ includes << entry
136
+ end
137
+ includes
138
+ end
139
+
140
+ # document.source_lines reflects content *after* skip-front-matter
141
+ # stripping, while every other category's starts_at comes from
142
+ # Asciidoctor's own line-number tracking, which counts the stripped
143
+ # front matter lines too. Compute that offset so line numbers stay
144
+ # consistent across the whole skim.
145
+ def source_line_offset document
146
+ front_matter = document.attr('front-matter')
147
+ return 0 unless front_matter && !front_matter.empty?
148
+
149
+ front_matter.count("\n") + 1 + 2 # captured lines + both '---' delimiters
150
+ end
151
+
152
+ def parse_include_attrs attrs_str
153
+ attrs = {}
154
+ if (m = attrs_str.match(/\btags?=("[^"]*"|'[^']*'|\S+)/))
155
+ attrs[:tags] = m[1].delete('"\'')
156
+ end
157
+ if (m = attrs_str.match(/\bleveloffset=("[^"]*"|'[^']*'|\S+)/))
158
+ attrs[:leveloffset] = m[1].delete('"\'')
159
+ end
160
+ attrs
161
+ end
162
+
70
163
  def line_count_for file_path
71
164
  return nil unless file_path && File.exist?(file_path)
72
165
 
@@ -6,6 +6,7 @@ require_relative 'yaml_frontmatter'
6
6
  require_relative 'source_skim/config'
7
7
  require_relative 'source_skim/skimmer'
8
8
  require_relative 'source_skim/markdown_skimmer'
9
+ require_relative 'source_skim/ruby_skimmer'
9
10
 
10
11
  module Sourcerer
11
12
  # SourceSkim produces machine-oriented skims of markup source documents.
@@ -57,11 +58,15 @@ module Sourcerer
57
58
  # @param attributes [Hash{String => String}] AsciiDoc only. Asciidoctor
58
59
  # attribute overrides. Silently ignored for Markdown.
59
60
  # @return [Hash] JSON-ready skim
60
- def self.skim_file file_path, forms: nil, format: nil, categories: nil, attributes: {}
61
+ def self.skim_file file_path, forms: nil, format: nil, categories: nil, attributes: {}, descriptions: false
61
62
  fmt = format || detect_format(file_path)
62
63
  if fmt == :markdown
63
64
  config = Config.new(forms: forms || [:flat])
64
65
  MarkdownSkimmer.new.process(File.read(file_path), config: config)
66
+ elsif fmt == :ruby
67
+ RubySkimmer.new.process(
68
+ File.read(file_path),
69
+ config: Config.new(forms: forms || [:flat], descriptions: descriptions))
65
70
  else
66
71
  attrs = LOAD_OPTS[:attributes].merge(attributes)
67
72
  opts = LOAD_OPTS.merge(attributes: attrs)
@@ -82,10 +87,12 @@ module Sourcerer
82
87
  # @param categories [Array<Symbol>, nil] AsciiDoc only
83
88
  # @param attributes [Hash{String => String}] AsciiDoc only
84
89
  # @return [Hash] JSON-ready skim
85
- def self.skim_string content, format: :asciidoc, forms: nil, categories: nil, attributes: {}
90
+ def self.skim_string content, format: :asciidoc, forms: nil, categories: nil, attributes: {}, descriptions: false
86
91
  if format == :markdown
87
92
  config = Config.new(forms: forms || [:flat])
88
93
  MarkdownSkimmer.new.process(content, config: config)
94
+ elsif format == :ruby
95
+ RubySkimmer.new.process(content, config: Config.new(forms: forms || [:flat], descriptions: descriptions))
89
96
  else
90
97
  attrs = LOAD_OPTS[:attributes].merge(attributes)
91
98
  opts = LOAD_OPTS.merge(attributes: attrs)
@@ -113,6 +120,8 @@ module Sourcerer
113
120
  ext = File.extname(file_path).downcase
114
121
  if Sourcerer::MARKDOWN_EXTS.include?(ext)
115
122
  :markdown
123
+ elsif ext == '.rb'
124
+ :ruby
116
125
  else
117
126
  :asciidoc
118
127
  end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sourcerer
4
+ module Util
5
+ # Resolve <tt>gem://</tt> URIs to absolute filesystem paths.
6
+ #
7
+ # Not required internally; callers must require this file explicitly.
8
+ module GemUri
9
+ GEM_URI_PATTERN = %r{\Agem://([^/]+)/(.+)\z}
10
+
11
+ # Resolve a +gem://+ URI to an absolute path within the named gem's directory.
12
+ # Returns +path+ unchanged if it is not a +gem://+ URI.
13
+ #
14
+ # URI format: <tt>gem://<gem-name>/<path-within-gem></tt>
15
+ # Example: <tt>gem://schemagraphy/lib/schemagraphy/cfgyml/templates/foo.liquid</tt>
16
+ #
17
+ # @param path [String] A gem:// URI or any other string.
18
+ # @return [String] The resolved absolute path, or the original string if not a gem:// URI.
19
+ # @raise [ArgumentError] If the gem:// URI is malformed.
20
+ # @raise [LoadError] If the referenced gem is not loaded.
21
+ def self.resolve path
22
+ return path unless path.is_a?(String) && path.start_with?('gem://')
23
+
24
+ match = GEM_URI_PATTERN.match(path)
25
+ raise ArgumentError, "Invalid gem:// URI: #{path}" unless match
26
+
27
+ spec = Gem.loaded_specs[match[1]]
28
+ raise LoadError, "Gem '#{match[1]}' not loaded (referenced in gem:// URI: #{path})" unless spec
29
+
30
+ File.join(spec.gem_dir, match[2])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Sourcerer
4
- VERSION = '0.3.1'
4
+ VERSION = '0.5.0'
5
5
  end
data/lib/sourcerer.rb CHANGED
@@ -23,7 +23,6 @@ module Sourcerer
23
23
  # File extensions recognised as AsciiDoc source files.
24
24
  ASCIIDOC_EXTS = %w[.adoc .asciidoc .asc .ad].freeze
25
25
 
26
- autoload :AttributesFilter, 'sourcerer/attributes_filter'
27
26
  autoload :YamlFrontmatter, 'sourcerer/yaml_frontmatter'
28
27
  autoload :Jekyll, 'sourcerer/jekyll'
29
28
  autoload :MarkDownGrade, 'sourcerer/mark_down_grade'