kramdown-ansi 0.5.0 → 0.7.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: 737af675856867287e33a53bf18bbb08517b3743900132f826b77cdad9b47c01
4
- data.tar.gz: f533ff7b8927579aead7c482039813091cf55796621febd5658a5c469d7e0957
3
+ metadata.gz: 6d35fcfbc735d86a3f58f59c1eaded0ebdc4f706db3cedb0c807aa6f9f420fe8
4
+ data.tar.gz: cd32746d6d592ee48e59bda8211adf670bdef612a5c65da97e114234ef8f0e83
5
5
  SHA512:
6
- metadata.gz: 5578a1e962bf25fbc54f00be909cb53a647bc3e56f67934a984c37f8c4a156e1dd499e446aa549817c35fd83c26c31a0f975319b06dcc7e556e73ba39b5be9cf
7
- data.tar.gz: b48b3cab8c7d2c05181efe12cd794be25a59d1319aaaa6ad26ae2a31b7a54311153f24076ae109a696c5cec791c958b6e6eba84bc2f3c9823c5feec9916ee825
6
+ metadata.gz: d7607e68a92ac9d968febb1179c60a291550306b56ada967fd29f2c9f1dd413f6d18981895b358b5a0ac34bdaae0291d1ed7e452a76dbe52329582fc11b11dc5
7
+ data.tar.gz: 07b078aad8c0e3d0b9cf1b509b9f21dc4a2cbdbf3101f4997a079349fb7272848fe58080a18ab4e79c4b13b07061cbe61dfb43c511b8630eb73c077a6ecb866b
data/.utilsrc CHANGED
@@ -41,7 +41,6 @@ code_indexer do
41
41
  date
42
42
  debug
43
43
  diff-lcs
44
- docile
45
44
  erb
46
45
  excon
47
46
  fileutils
@@ -79,8 +78,6 @@ code_indexer do
79
78
  search_ui
80
79
  shellwords
81
80
  simplecov
82
- simplecov-html
83
- simplecov_json_formatter
84
81
  stringio
85
82
  sync
86
83
  term-ansicolor
data/CHANGES.md CHANGED
@@ -1,5 +1,99 @@
1
1
  # Changes
2
2
 
3
+ ## 2026-10-22 v0.7.0
4
+
5
+ ### Fixes
6
+
7
+ - Fixed GFM table parsing for non-header tables by rendering `:table` elements
8
+ without a `:thead` as plain text, as GFM requires a header row and delimiter
9
+ row for a valid table.
10
+ - Corrected table continuation detection in `self.parse` by checking if the
11
+ next line contains a `|`, resolving issues where delimiter or body rows
12
+ lacked a leading pipe (e.g., `:---|---:`, `1 | 2`).
13
+
14
+ ### Improvements
15
+
16
+ - Replaced `Regexp.last_match` calls with direct captured group assignments
17
+ (`$1`, `$2`) in `ansi.rb` for clarity.
18
+
19
+ ### Build & CI
20
+
21
+ - Added `scripts/capture-ansi` and `.envrc` for local golden file regeneration.
22
+ - Added `.envrc` and `scripts/*` to `package_ignore` in `Rakefile`.
23
+
24
+ ### Testing
25
+
26
+ - Added Case 27 (pipe in a sentence) to `spec/assets/tables.md` and regenerated
27
+ `spec/assets/tables.ansi`.
28
+
29
+ ## 2026-10-21 v0.6.0
30
+
31
+ ### Features
32
+
33
+ - Add support for nested blockquotes with cycling typographic quotation marks
34
+ based on nesting depth.
35
+ - Add ANSI styling support for HTML elements: `ins`/`u` (underline), `mark`
36
+ (negative/inverse), `tt` (code), `small` (faint), and passthrough for
37
+ `span`/`sub`/`sup`.
38
+ - Add strikethrough support for `del` and `s` HTML elements.
39
+ - Introduce `ProtectEnvVars` module in test helper to isolate environment
40
+ variable changes during tests tagged with `:protect_env`.
41
+
42
+ ### Fixes
43
+
44
+ - Fix GFM table parsing by inserting required blank lines before and after pipe
45
+ tables to ensure correct recognition by `kramdown`.
46
+ - Fix `Integer#size` regression in table column width calculation by using
47
+ `display_width` directly instead of `.size`, ensuring correct column ratios
48
+ on Ruby 4.
49
+ - Fix empty table cell rendering by replacing empty strings with a single space
50
+ and falling back to equal-width splits when total width is zero.
51
+ - Fix typo in `convert_html_element` where `e.value` was used instead of
52
+ `el.value` for the `del`/`s` branch.
53
+ - Make `Token` definition in `Kramdown::ANSI::Width` compatible with Ruby
54
+ versions lacking `Data` by providing a `Struct`-based fallback.
55
+
56
+ ### Improvements
57
+
58
+ - Rewrite `Kramdown::ANSI::Width#truncate` to be ANSI-aware, correctly handling
59
+ escape sequences, wide characters (emojis), and zero-width characters using
60
+ `display_width`.
61
+ - Optimize `wrap` and `truncate` methods by using `scan` instead of `gsub` to
62
+ reduce allocation.
63
+ - Generalize the `capture:tables` Rake task to `capture:ansi` to regenerate
64
+ golden files for all `spec/assets/*.md` fixtures.
65
+ - Update `README.md` style examples to include `blockquote` and add a nested
66
+ dialog example.
67
+
68
+ ### Build & CI
69
+
70
+ - Add `tins` gem (`~> 1.61`) as a runtime dependency.
71
+ - Upgrade `gem_hadar` development dependency to `>= 2.20.1`.
72
+ - Update `rubygems_version` in gemspec to **4.0.22**.
73
+ - Remove unused gems (`docile`, `simplecov-html`, `simplecov_json_formatter`)
74
+ from `.utilsrc`.
75
+ - Add `git config --global --add safe.directory /work` to Docker test script.
76
+ - Use `nproc` instead of `getconf _NPROCESSORS_ONLN` for CPU count in Docker
77
+ test script.
78
+ - Add `tmp/.gitkeep` to track the empty `tmp` directory.
79
+
80
+ ### Testing
81
+
82
+ - Add `spec/kramdown/render_spec.rb` to test `Kramdown::ANSI.parse` for
83
+ headers, inline emphasis, code blocks, blockquotes, horizontal rules, lists,
84
+ links, images, and paragraphs.
85
+ - Add `spec/assets/tables.md` and `spec/assets/tables.ansi` golden files
86
+ covering 26 edge cases for table rendering.
87
+ - Add `spec/kramdown/ansi_golden_spec.rb` for output regression testing against
88
+ golden files.
89
+ - Add `spec/assets/html.md` and `spec/assets/html.ansi` golden files covering
90
+ HTML element styling.
91
+ - Add 9 new specs for `Kramdown::ANSI::Width#truncate` covering wide emojis,
92
+ empty strings, pure ANSI, zero-width characters, newline preservation,
93
+ multi-cell ellipsis, nested-style reset, and post-cut escape omission.
94
+ - Stub `Tins::Terminal.columns` to 80 in `ansi_spec.rb` and `render_spec.rb`
95
+ for deterministic output.
96
+
3
97
  ## 2026-03-21 v0.5.0
4
98
 
5
99
  - Added `spec/kramdown/ansi_table_spec.rb` to the test suite and gemspec file
data/README.md CHANGED
@@ -47,10 +47,11 @@ You can customize the ANSI styles by passing an `ansi_styles` option:
47
47
  require 'kramdown/ansi'
48
48
 
49
49
  ansi_styles = {
50
- header: [:bold, :magenta],
51
- strong: :green,
52
- em: :yellow,
53
- code: [:cyan, :underline]
50
+ header: [:bold, :magenta],
51
+ strong: :green,
52
+ em: :yellow,
53
+ code: [:cyan, :underline],
54
+ blockquote: :blue,
54
55
  }
55
56
 
56
57
  puts Kramdown::ANSI.parse(markdown, ansi_styles:)
@@ -65,7 +66,7 @@ methods:
65
66
  require 'kramdown/ansi'
66
67
 
67
68
  # Using from_json method
68
- json_config = '{"header": ["bold", "magenta"], "code": ["cyan", "underline"]}'
69
+ json_config = '{"header": ["bold", "magenta"], "code": ["cyan", "underline"], "blockquote": ["magenta", "underline"]}'
69
70
  ansi_styles = Kramdown::ANSI::Styles.from_json(json_config)
70
71
 
71
72
  # Apply to parsing
@@ -177,6 +178,10 @@ The homepage of this library is located at
177
178
 
178
179
  ![cat](./spec/assets/kitten.jpg)
179
180
 
181
+ Dialog:
182
+ > What's that, Fluffy?
183
+ >> That's my new friend.
184
+
180
185
  ---
181
186
 
182
187
  This is the end.
data/Rakefile CHANGED
@@ -19,7 +19,7 @@ GemHadar do
19
19
  '.yardoc', 'doc', 'tags', 'errors.lst', 'cscope.out', 'coverage', 'tmp',
20
20
  'yard'
21
21
  package_ignore '.all_images.yml', '.tool-versions', '.gitignore', 'VERSION',
22
- '.rspec', '.github'
22
+ '.rspec', '.github', '.envrc', 'scripts/*'
23
23
  readme 'README.md'
24
24
 
25
25
  executables << 'md' << 'git-md'
@@ -37,6 +37,7 @@ GemHadar do
37
37
  dependency 'term-ansicolor', '~> 1.11'
38
38
  dependency 'kramdown-parser-gfm', '~> 1.1'
39
39
  dependency 'terminal-table', '~> 4.0'
40
+ dependency 'tins', '~> 1.61'
40
41
  dependency 'json', '~> 2.0'
41
42
  dependency 'unicode-display_width', '~> 3.0'
42
43
  development_dependency 'all_images', '~> 0.4'
@@ -48,3 +49,21 @@ GemHadar do
48
49
 
49
50
  clobber 'coverage'
50
51
  end
52
+
53
+ namespace :capture do
54
+ desc 'Capture golden file ansi output for all spec/assets/*.md files'
55
+ task :ansi do
56
+ require 'kramdown/ansi'
57
+ module Tins::Terminal
58
+ def self.columns
59
+ 80
60
+ end
61
+ end
62
+ Dir[File.join('spec', 'assets', '*.md')].sort.each do |md|
63
+ out_path = md.sub('.md', '.ansi')
64
+ out = Kramdown::ANSI.parse(File.read(md))
65
+ File.write(out_path, out)
66
+ puts "Captured #{out_path} (#{out.bytesize} bytes)"
67
+ end
68
+ end
69
+ end
@@ -1,9 +1,9 @@
1
1
  # -*- encoding: utf-8 -*-
2
- # stub: kramdown-ansi 0.5.0 ruby lib
2
+ # stub: kramdown-ansi 0.7.0 ruby lib
3
3
 
4
4
  Gem::Specification.new do |s|
5
5
  s.name = "kramdown-ansi".freeze
6
- s.version = "0.5.0".freeze
6
+ s.version = "0.7.0".freeze
7
7
 
8
8
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
9
9
  s.require_paths = ["lib".freeze]
@@ -13,18 +13,18 @@ Gem::Specification.new do |s|
13
13
  s.email = "flori@ping.de".freeze
14
14
  s.executables = ["md".freeze, "git-md".freeze]
15
15
  s.extra_rdoc_files = ["README.md".freeze, "lib/kramdown/ansi.rb".freeze, "lib/kramdown/ansi/pager.rb".freeze, "lib/kramdown/ansi/styles.rb".freeze, "lib/kramdown/ansi/width.rb".freeze, "lib/kramdown/version.rb".freeze]
16
- s.files = [".utilsrc".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/git-md".freeze, "bin/md".freeze, "img/git-md.png".freeze, "img/md.png".freeze, "kramdown-ansi.gemspec".freeze, "lib/kramdown/ansi.rb".freeze, "lib/kramdown/ansi/pager.rb".freeze, "lib/kramdown/ansi/styles.rb".freeze, "lib/kramdown/ansi/width.rb".freeze, "lib/kramdown/version.rb".freeze, "spec/assets/README.ansi".freeze, "spec/assets/kitten.jpg".freeze, "spec/kramdown/ansi/pager_spec.rb".freeze, "spec/kramdown/ansi/styles_spec.rb".freeze, "spec/kramdown/ansi/width_spec.rb".freeze, "spec/kramdown/ansi_spec.rb".freeze, "spec/kramdown/ansi_table_spec.rb".freeze, "spec/spec_helper.rb".freeze]
16
+ s.files = [".utilsrc".freeze, "CHANGES.md".freeze, "Gemfile".freeze, "LICENSE".freeze, "README.md".freeze, "Rakefile".freeze, "bin/git-md".freeze, "bin/md".freeze, "img/git-md.png".freeze, "img/md.png".freeze, "kramdown-ansi.gemspec".freeze, "lib/kramdown/ansi.rb".freeze, "lib/kramdown/ansi/pager.rb".freeze, "lib/kramdown/ansi/styles.rb".freeze, "lib/kramdown/ansi/width.rb".freeze, "lib/kramdown/version.rb".freeze, "scripts/capture-ansi".freeze, "spec/assets/README.ansi".freeze, "spec/assets/html.ansi".freeze, "spec/assets/html.md".freeze, "spec/assets/kitten.jpg".freeze, "spec/assets/tables.ansi".freeze, "spec/assets/tables.md".freeze, "spec/kramdown/ansi/pager_spec.rb".freeze, "spec/kramdown/ansi/styles_spec.rb".freeze, "spec/kramdown/ansi/width_spec.rb".freeze, "spec/kramdown/ansi_golden_spec.rb".freeze, "spec/kramdown/ansi_spec.rb".freeze, "spec/kramdown/ansi_table_spec.rb".freeze, "spec/kramdown/render_spec.rb".freeze, "spec/spec_helper.rb".freeze, "tmp/.gitkeep".freeze]
17
17
  s.homepage = "https://github.com/flori/kramdown-ansi".freeze
18
18
  s.licenses = ["MIT".freeze]
19
19
  s.rdoc_options = ["--title".freeze, "Kramdown-ansi - Output markdown in the terminal with ANSI escape sequences".freeze, "--main".freeze, "README.md".freeze]
20
20
  s.required_ruby_version = Gem::Requirement.new(">= 3.1".freeze)
21
- s.rubygems_version = "4.0.8".freeze
21
+ s.rubygems_version = "4.0.22".freeze
22
22
  s.summary = "Output markdown in the terminal with ANSI escape sequences".freeze
23
- s.test_files = ["spec/kramdown/ansi/pager_spec.rb".freeze, "spec/kramdown/ansi/styles_spec.rb".freeze, "spec/kramdown/ansi/width_spec.rb".freeze, "spec/kramdown/ansi_spec.rb".freeze, "spec/kramdown/ansi_table_spec.rb".freeze, "spec/spec_helper.rb".freeze]
23
+ s.test_files = ["spec/kramdown/ansi/pager_spec.rb".freeze, "spec/kramdown/ansi/styles_spec.rb".freeze, "spec/kramdown/ansi/width_spec.rb".freeze, "spec/kramdown/ansi_golden_spec.rb".freeze, "spec/kramdown/ansi_spec.rb".freeze, "spec/kramdown/ansi_table_spec.rb".freeze, "spec/kramdown/render_spec.rb".freeze, "spec/spec_helper.rb".freeze]
24
24
 
25
25
  s.specification_version = 4
26
26
 
27
- s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.17.0".freeze])
27
+ s.add_development_dependency(%q<gem_hadar>.freeze, [">= 2.20.1".freeze])
28
28
  s.add_development_dependency(%q<all_images>.freeze, ["~> 0.4".freeze])
29
29
  s.add_development_dependency(%q<rspec>.freeze, ["~> 3.2".freeze])
30
30
  s.add_development_dependency(%q<debug>.freeze, [">= 0".freeze])
@@ -32,6 +32,7 @@ Gem::Specification.new do |s|
32
32
  s.add_runtime_dependency(%q<term-ansicolor>.freeze, ["~> 1.11".freeze])
33
33
  s.add_runtime_dependency(%q<kramdown-parser-gfm>.freeze, ["~> 1.1".freeze])
34
34
  s.add_runtime_dependency(%q<terminal-table>.freeze, ["~> 4.0".freeze])
35
+ s.add_runtime_dependency(%q<tins>.freeze, ["~> 1.61".freeze])
35
36
  s.add_runtime_dependency(%q<json>.freeze, ["~> 2.0".freeze])
36
37
  s.add_runtime_dependency(%q<unicode-display_width>.freeze, ["~> 3.0".freeze])
37
38
  end
@@ -24,10 +24,15 @@ class Kramdown::ANSI::Styles
24
24
  # configuration.
25
25
  def initialize
26
26
  @default_ansi_styles = {
27
- header: %i[ bold underline ],
28
- strong: :bold,
29
- em: :italic,
30
- code: :blue,
27
+ header: %i[ bold underline ],
28
+ strong: :bold,
29
+ em: :italic,
30
+ code: :blue,
31
+ blockquote: :faint,
32
+ del: :strikethrough,
33
+ ins: :underline,
34
+ mark: :negative,
35
+ small: :faint,
31
36
  }
32
37
  @ansi_styles = @default_ansi_styles.dup
33
38
  end
@@ -19,20 +19,24 @@ module Kramdown::ANSI::Width
19
19
 
20
20
  module_function
21
21
 
22
- # Returns the width of the terminal in characters, given a percentage.
23
- #
24
- # @param percentage [Numeric] the percentage value (defaults to 100.0)
25
- # @return [Integer] the calculated width
26
- def width(percentage: 100.0)
27
- ((Float(percentage) * Tins::Terminal.columns) / 100).floor
28
- end
22
+ ANSI_ESCAPES = /\e\[.*?m|\e\].*?(\e|\a)\\?/
23
+
24
+ private_constant :ANSI_ESCAPES
29
25
 
30
26
  # Removes ANSI escape sequences from the given string.
31
27
  #
32
28
  # @param line [String] the string potentially containing ANSI escape codes.
33
29
  # @return [String] the string with all ANSI escape sequences removed.
34
30
  def remove_ansi_escapes(line)
35
- line.to_s.gsub(/\e\[.*?m|\e\].*?(\e|\a)\\?/, '')
31
+ line.to_s.gsub(ANSI_ESCAPES, '')
32
+ end
33
+
34
+ # Returns the width of the terminal in characters, given a percentage.
35
+ #
36
+ # @param percentage [Numeric] the percentage value (defaults to 100.0)
37
+ # @return [Integer] the calculated width
38
+ def width(percentage: 100.0)
39
+ ((Float(percentage) * Tins::Terminal.columns) / 100).floor
36
40
  end
37
41
 
38
42
  # Calculates the display width of a string after removing ANSI escape
@@ -67,8 +71,8 @@ module Kramdown::ANSI::Width
67
71
  end
68
72
  length = length.clamp(1..)
69
73
  length_without_ansi = 0
70
- result = ''
71
- text.gsub(/(?<nws>\S+)|(?<ws>\s)/) do
74
+ result = +''
75
+ text.scan(/(?<nws>\S+)|(?<ws>\s)/) do
72
76
  token_value = $&
73
77
  token_length = display_width(token_value)
74
78
  if $~[:nws]
@@ -91,6 +95,28 @@ module Kramdown::ANSI::Width
91
95
  result
92
96
  end
93
97
 
98
+ if defined?(Data)
99
+ # A frozen data record for a single token in the `truncate` scan.
100
+ #
101
+ # +type+ is `:esc` (ANSI escape, zero display width) or
102
+ # `:txt` (visible character, display width from
103
+ # `Unicode::DisplayWidth`).
104
+ # +value+ is the raw matched substring.
105
+ # +length+ is the display width in terminal columns (0 for escapes).
106
+ Token = Data.define(:type, :value, :length)
107
+ else
108
+ class Token < Struct.new(:type, :value, :length)
109
+ def initialize(**opts)
110
+ opts.each do |key, value|
111
+ self.send("#{key}=", value)
112
+ end
113
+ freeze
114
+ end
115
+ end
116
+ end
117
+
118
+ private_constant :Token
119
+
94
120
  # Truncates a given string to a specified length or percentage. If the text
95
121
  # is longer an ellipsis sequence is added at the end of the generated string,
96
122
  # to indicate that a truncation has been performed.
@@ -105,13 +131,59 @@ module Kramdown::ANSI::Width
105
131
  percentage.nil? ^ length.nil? or
106
132
  raise ArgumentError, "either pass percentage or length argument"
107
133
  percentage and length ||= width(percentage:)
108
- ellipsis_length = ellipsis.size
109
- if length < ellipsis_length
110
- +''
111
- elsif text.size >= length + ellipsis_length
112
- text[0, length - ellipsis_length] + ellipsis
113
- else
114
- text
134
+ ellipsis_length = display_width(remove_ansi_escapes(ellipsis))
135
+ found_esc = false
136
+ result = +''
137
+
138
+ return result if text.empty?
139
+ return result if length < ellipsis_length
140
+
141
+ current_length = 0
142
+ tokens = []
143
+
144
+ text.scan(/(?<esc>#{ANSI_ESCAPES})|(?<txt>.)/m) do
145
+ token = Token.new(
146
+ type: $~[:esc] ? :esc : :txt,
147
+ value: $&,
148
+ length: $~[:esc] ? 0 : display_width($&)
149
+ )
150
+ tokens << token
151
+ end
152
+
153
+ while token = tokens.shift
154
+ if token.type == :esc
155
+ found_esc = true
156
+ result << token.value
157
+ next
158
+ end
159
+
160
+ if current_length + token.length == length && tokens.empty?
161
+ result << token.value
162
+ current_length += token.length
163
+ break
164
+ elsif current_length + token.length + ellipsis_length > length
165
+ result << ellipsis
166
+ break
167
+ elsif current_length + token.length + ellipsis_length == length
168
+ result << token.value
169
+ current_length += token.length
170
+ remaining_text_length = tokens.select { _1.type == :txt }.map(&:length).sum
171
+ if current_length + remaining_text_length == length
172
+ next
173
+ elsif current_length + remaining_text_length > length
174
+ result << ellipsis
175
+ break
176
+ else
177
+ next
178
+ end
179
+ else
180
+ current_length += token.length
181
+ result << token.value
182
+ end
183
+
115
184
  end
185
+
186
+ result << reset if found_esc && !result.end_with?(reset)
187
+ result
116
188
  end
117
189
  end
data/lib/kramdown/ansi.rb CHANGED
@@ -91,6 +91,34 @@ class Kramdown::ANSI < Kramdown::Converter::Base
91
91
  options = { input: :mygfm, auto_ids: false, entity_output: :as_char }.merge(
92
92
  options.transform_keys(&:to_sym)
93
93
  )
94
+ # Insert blank line before GFM pipe tables glued to preceding
95
+ # content; kramdown requires it to parse the table correctly.
96
+ #
97
+ # Matches: a non-table line immediately followed by a table header,
98
+ # a valid GFM separator row, and at least one body row.
99
+ # A two-line "table" (header + separator, no body) is not a table
100
+ # per GFM, so no blank line is inserted.
101
+ source = source.gsub(
102
+ /
103
+ ( [^|\n][^\n]*\n ) # $1: preceding line (not a pipe line)
104
+ ( \|[^\n]*\n ) # $2: table header (starts with |)
105
+ ( \|(?:\s*:?-+:?\s*\|)+\n ) # $3: GFM separator + newline
106
+ ( \|[^\n]* ) # $4: first body row (starts with |)
107
+ \n
108
+ /x,
109
+ "\\1\n\\2\\3\\4\n"
110
+ )
111
+ # Insert blank line after a table's last row when glued to
112
+ # following content; kramdown requires it to close the table.
113
+ #
114
+ # A following line that contains a pipe is likely a table row
115
+ # (separator or body), so we keep it glued.
116
+ source = source.gsub(
117
+ /(\|[^\n]*\n)([^|\n][^\n]*)/
118
+ ) do |match|
119
+ row, next_line = $1, $2
120
+ next_line.include?('|') ? match : "#{row}\n#{next_line}"
121
+ end
94
122
  @doc = Kramdown::Document.new(source, options).to_ansi
95
123
  end
96
124
 
@@ -253,6 +281,28 @@ class Kramdown::ANSI < Kramdown::Converter::Base
253
281
  apply_style(:code) { el.value }
254
282
  end
255
283
 
284
+ BLOCKQUOTE_QUOTES = [
285
+ %w[“ ”],
286
+ %w[‘ ’],
287
+ %w[ » « ],
288
+ %w[ › ‹ ],
289
+ ].map(&:freeze).freeze
290
+ private_constant :BLOCKQUOTE_QUOTES
291
+
292
+ # Wraps the content of a blockquote level with the corresponding
293
+ # typographic quotation marks, cycling through a predefined set of
294
+ # quote pairs based on the nesting depth.
295
+ #
296
+ # @param level [ Integer ] the nesting level of the blockquote
297
+ # @yield [ String ] the inner content to be wrapped in quotation marks
298
+ # @return [ String ] the content enclosed by the appropriate opening and
299
+ # closing quote characters
300
+ def blockquote_quote(level, &block)
301
+ quote = BLOCKQUOTE_QUOTES[level % BLOCKQUOTE_QUOTES.size]
302
+ quote[0] + block.() + quote[1]
303
+ end
304
+ private :blockquote_quote
305
+
256
306
  # The convert_blockquote method processes a blockquote element by applying
257
307
  # proper formatting.
258
308
  #
@@ -266,7 +316,19 @@ class Kramdown::ANSI < Kramdown::Converter::Base
266
316
  # @return [String] the formatted blockquote content with quotation marks and
267
317
  # proper newlines
268
318
  def convert_blockquote(el, opts)
269
- newline ?“ + inner(el, opts).sub(/\n+\z/, '') + ?”
319
+ old_level = opts[:blockquote_level] ||= -1
320
+ level = opts[:blockquote_level] = old_level + 1
321
+ if level.zero?
322
+ newline(
323
+ blockquote_quote(level) {
324
+ apply_style(:blockquote) { inner(el, opts).strip }
325
+ }.gsub(/^/, ' ')
326
+ )
327
+ else
328
+ blockquote_quote(level) { inner(el, opts).strip }.gsub(/^/, ' ')
329
+ end
330
+ ensure
331
+ opts[:blockquote_level] = old_level
270
332
  end
271
333
 
272
334
  # The convert_hr method processes a horizontal rule element by generating a
@@ -373,6 +435,18 @@ class Kramdown::ANSI < Kramdown::Converter::Base
373
435
  apply_style(:em) { inner(el, opts) }
374
436
  elsif el.value == 'b' || el.value == 'strong'
375
437
  apply_style(:strong) { inner(el, opts) }
438
+ elsif el.value == 'ins' || el.value == 'u'
439
+ apply_style(:ins) { inner(el, opts) }
440
+ elsif el.value == 'mark'
441
+ apply_style(:mark) { inner(el, opts) }
442
+ elsif el.value == 'del' || el.value == 's'
443
+ apply_style(:del) { inner(el, opts) }
444
+ elsif el.value == 'tt'
445
+ apply_style(:code) { inner(el, opts) }
446
+ elsif el.value == 'small'
447
+ apply_style(:small) { inner(el, opts) }
448
+ elsif %w[ span sub sup ].include?(el.value)
449
+ inner(el, opts)
376
450
  else
377
451
  ''
378
452
  end
@@ -386,6 +460,18 @@ class Kramdown::ANSI < Kramdown::Converter::Base
386
460
  # @param opts [Hash] conversion options containing context for processing
387
461
  # @return [String] the formatted table content with proper styling and newlines
388
462
  def convert_table(el, opts)
463
+ # Kramdown's base parser is more permissive than GFM: it accepts
464
+ # any pipe-delimited line as a table row, even without a header.
465
+ # GFM mandates a header + separator row minimum, so a :table
466
+ # element without :thead is just pipe-separated prose.
467
+ unless el.children.any? { |c| c.type == :thead }
468
+ rows = el.children.select { |c| c.type == :tbody }.flat_map do |tbody|
469
+ tbody.children.map do |row|
470
+ row.children.map { |cell| convert(cell, opts).strip }.join(' | ')
471
+ end
472
+ end
473
+ return newline rows.join("\n")
474
+ end
389
475
  table = Terminal::Table.new
390
476
  table.style = {
391
477
  all_separators: true,
@@ -457,13 +543,22 @@ class Kramdown::ANSI < Kramdown::Converter::Base
457
543
  # addition of the row to the table instance
458
544
  def convert_tr(el, opts)
459
545
  return '' if el.children.empty?
460
- cols = el.children.map { |c| convert(c, opts).strip }
546
+ cols = el.children.map do |c|
547
+ stripped = convert(c, opts).strip
548
+ stripped.empty? ? ' ' : stripped
549
+ end
461
550
  return '' if cols.empty?
462
- col_widths = cols.map { |c| display_width(c).size }
551
+ col_widths = cols.map { |c| display_width(c) }
463
552
  col_widths_sum = col_widths.sum
464
- full_width = width(percentage: 90)
465
- ratios = cols.each_with_index.map { |c, i| col_widths[i] / col_widths_sum.to_f }
466
- wrapped_cols = cols.zip(ratios).map { |c, r| wrap(c, length: r * full_width) }
553
+ full_width = width(percentage: 90)
554
+ ratios = cols.each_with_index.map do |c, i|
555
+ if col_widths_sum.zero?
556
+ 1.0 / cols.size
557
+ else
558
+ col_widths[i] / col_widths_sum.to_f
559
+ end
560
+ end
561
+ wrapped_cols = cols.zip(ratios).map { |c, r| wrap(c, length: r * full_width) }
467
562
  opts[:table] << wrapped_cols
468
563
  ''
469
564
  end
@@ -1,6 +1,11 @@
1
+ # Version information for the Kramdown gem.
2
+ #
3
+ # This module is auto-generated by GemHadar and exposes the
4
+ # current release version as a set of constants. Do not edit it
5
+ # by hand; it is rewritten on every `rake version` invocation.
1
6
  module Kramdown
2
- # Kramdown version
3
- VERSION = '0.5.0'
7
+ # The current version of the Kramdown gem as a String.
8
+ VERSION = '0.7.0'
4
9
  VERSION_ARRAY = VERSION.split('.').map(&:to_i) # :nodoc:
5
10
  VERSION_MAJOR = VERSION_ARRAY[0] # :nodoc:
6
11
  VERSION_MINOR = VERSION_ARRAY[1] # :nodoc:
@@ -0,0 +1,3 @@
1
+ #!/bin/sh
2
+
3
+ exec bundle exec ruby -I lib -S rake capture:ansi
@@ -0,0 +1,28 @@
1
+ HTML Elements
2
+
3
+ Emphasis
4
+
5
+ italic i and italic em
6
+
7
+ Strong
8
+
9
+ bold b and bold strong
10
+
11
+ Strikethrough
12
+
13
+ strikethrough and html s tag and del tag
14
+
15
+ Underline
16
+
17
+ Code
18
+
19
+ tt text
20
+
21
+ Mark
22
+
23
+ marked
24
+
25
+ Passthrough (no style)
26
+
27
+ span text and subscript and superscript
28
+ and small text
@@ -0,0 +1,28 @@
1
+ # HTML Elements
2
+
3
+ ## Emphasis
4
+
5
+ <i>italic i</i> and <em>italic em</em>
6
+
7
+ ## Strong
8
+
9
+ <b>bold b</b> and <strong>bold strong</strong>
10
+
11
+ ## Strikethrough
12
+
13
+ ~~strikethrough~~ and <s>html s tag</s> and <del>del tag</del>
14
+
15
+ ## Underline
16
+
17
+ ## Code
18
+
19
+ <tt>tt text</tt>
20
+
21
+ ## Mark
22
+
23
+ <mark>marked</mark>
24
+
25
+ ## Passthrough (no style)
26
+
27
+ <span>span text</span> and <sub>subscript</sub> and <sup>superscript</sup>
28
+ and <small>small text</small>