kramdown-ansi 0.4.1 → 0.6.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 +4 -4
- data/.utilsrc +96 -0
- data/CHANGES.md +96 -0
- data/README.md +10 -5
- data/Rakefile +21 -1
- data/kramdown-ansi.gemspec +9 -7
- data/lib/kramdown/ansi/styles.rb +9 -4
- data/lib/kramdown/ansi/width.rb +132 -16
- data/lib/kramdown/ansi.rb +94 -10
- data/lib/kramdown/version.rb +7 -2
- data/spec/assets/html.ansi +28 -0
- data/spec/assets/html.md +28 -0
- data/spec/assets/tables.ansi +348 -0
- data/spec/assets/tables.md +236 -0
- data/spec/kramdown/ansi/pager_spec.rb +0 -2
- data/spec/kramdown/ansi/styles_spec.rb +0 -3
- data/spec/kramdown/ansi/width_spec.rb +90 -2
- data/spec/kramdown/ansi_golden_spec.rb +19 -0
- data/spec/kramdown/ansi_spec.rb +8 -6
- data/spec/kramdown/ansi_table_spec.rb +51 -0
- data/spec/kramdown/render_spec.rb +156 -0
- data/spec/spec_helper.rb +96 -0
- data/tmp/.gitkeep +0 -0
- metadata +46 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0e9bbd4a032cd0f37629bc21f4967590fcb7140875fe79687e01f5941cdea10c
|
|
4
|
+
data.tar.gz: 2805ae302ae89c97266f7cfb690ab7a8b87662c6654e802d42ac7b9ae125515c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7268907b0b2467c70767f995d7cf1823d81378c2da7f5b9d324846b54ec63ad3357a91de8cfc116b49d5efd687a79537788d5c2e2cd2c0a5f66afa0715abbce6
|
|
7
|
+
data.tar.gz: 2a19614f212d5f2a8a55310d6ecc51d64050378a744dd0ea180b576f7da6a90d976ffb606ff62f4ab48d5e132425ca5616d17cb88479e5d555f5013c729db31a
|
data/.utilsrc
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# vim: set ft=ruby:
|
|
2
|
+
|
|
3
|
+
search do
|
|
4
|
+
prune_dirs /\A(\.svn|\.git|\.terraform|CVS|tmp|coverage|corpus|pkg|\.yardoc|doc)\z/
|
|
5
|
+
skip_files /(\A\.|\.sw[pon]\z|\.(log|fnm|jpg|jpeg|png|pdf|svg)\z|\A(tags|cscope\.out)\z|~\z)/i
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
discover do
|
|
9
|
+
prune_dirs /\A(\.svn|\.git|\.terraform|\.yardoc|CVS|tmp|coverage|corpus|pkg|\.yardoc|doc)\z/
|
|
10
|
+
skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
|
|
11
|
+
index_expire_after 3_600
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
strip_spaces do
|
|
15
|
+
prune_dirs /\A(\..*|CVS|pkg|\.yardoc)\z/
|
|
16
|
+
skip_files /(\A\.|\.sw[pon]\z|\.log\z|~\z)/
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
probe do
|
|
20
|
+
test_framework :rspec
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ssh_tunnel do
|
|
24
|
+
terminal_multiplexer :tmux
|
|
25
|
+
login_session "/home/#{ENV['USER']}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
classify do
|
|
29
|
+
shift_path_by_default 1
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
code_indexer do
|
|
33
|
+
verbose false
|
|
34
|
+
|
|
35
|
+
gems = %w[
|
|
36
|
+
all_images
|
|
37
|
+
amatch
|
|
38
|
+
base64
|
|
39
|
+
bigdecimal
|
|
40
|
+
complex_config
|
|
41
|
+
date
|
|
42
|
+
debug
|
|
43
|
+
diff-lcs
|
|
44
|
+
erb
|
|
45
|
+
excon
|
|
46
|
+
fileutils
|
|
47
|
+
gem_hadar
|
|
48
|
+
infobar
|
|
49
|
+
io-console
|
|
50
|
+
irb
|
|
51
|
+
json
|
|
52
|
+
kramdown
|
|
53
|
+
kramdown-ansi
|
|
54
|
+
kramdown-parser-gfm
|
|
55
|
+
logger
|
|
56
|
+
mize
|
|
57
|
+
more_math
|
|
58
|
+
net-http
|
|
59
|
+
nokogiri
|
|
60
|
+
ollama-ruby
|
|
61
|
+
openssl
|
|
62
|
+
ostruct
|
|
63
|
+
pp
|
|
64
|
+
prettyprint
|
|
65
|
+
prism
|
|
66
|
+
psych
|
|
67
|
+
racc
|
|
68
|
+
rake
|
|
69
|
+
rdoc
|
|
70
|
+
readline
|
|
71
|
+
reline
|
|
72
|
+
rexml
|
|
73
|
+
rspec
|
|
74
|
+
rspec-core
|
|
75
|
+
rspec-expectations
|
|
76
|
+
rspec-mocks
|
|
77
|
+
rspec-support
|
|
78
|
+
search_ui
|
|
79
|
+
shellwords
|
|
80
|
+
simplecov
|
|
81
|
+
stringio
|
|
82
|
+
sync
|
|
83
|
+
term-ansicolor
|
|
84
|
+
terminal-table
|
|
85
|
+
tins
|
|
86
|
+
tsort
|
|
87
|
+
unicode-display_width
|
|
88
|
+
uri
|
|
89
|
+
yaml
|
|
90
|
+
yard
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
paths {
|
|
94
|
+
%w[ lib ] + gems.map { `bundle show #{it}` }.map(&:chomp)
|
|
95
|
+
}
|
|
96
|
+
end
|
data/CHANGES.md
CHANGED
|
@@ -1,5 +1,101 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## 2026-10-21 v0.6.0
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Add support for nested blockquotes with cycling typographic quotation marks
|
|
8
|
+
based on nesting depth.
|
|
9
|
+
- Add ANSI styling support for HTML elements: `ins`/`u` (underline), `mark`
|
|
10
|
+
(negative/inverse), `tt` (code), `small` (faint), and passthrough for
|
|
11
|
+
`span`/`sub`/`sup`.
|
|
12
|
+
- Add strikethrough support for `del` and `s` HTML elements.
|
|
13
|
+
- Introduce `ProtectEnvVars` module in test helper to isolate environment
|
|
14
|
+
variable changes during tests tagged with `:protect_env`.
|
|
15
|
+
|
|
16
|
+
### Fixes
|
|
17
|
+
|
|
18
|
+
- Fix GFM table parsing by inserting required blank lines before and after pipe
|
|
19
|
+
tables to ensure correct recognition by `kramdown`.
|
|
20
|
+
- Fix `Integer#size` regression in table column width calculation by using
|
|
21
|
+
`display_width` directly instead of `.size`, ensuring correct column ratios
|
|
22
|
+
on Ruby 4.
|
|
23
|
+
- Fix empty table cell rendering by replacing empty strings with a single space
|
|
24
|
+
and falling back to equal-width splits when total width is zero.
|
|
25
|
+
- Fix typo in `convert_html_element` where `e.value` was used instead of
|
|
26
|
+
`el.value` for the `del`/`s` branch.
|
|
27
|
+
- Make `Token` definition in `Kramdown::ANSI::Width` compatible with Ruby
|
|
28
|
+
versions lacking `Data` by providing a `Struct`-based fallback.
|
|
29
|
+
|
|
30
|
+
### Improvements
|
|
31
|
+
|
|
32
|
+
- Rewrite `Kramdown::ANSI::Width#truncate` to be ANSI-aware, correctly handling
|
|
33
|
+
escape sequences, wide characters (emojis), and zero-width characters using
|
|
34
|
+
`display_width`.
|
|
35
|
+
- Optimize `wrap` and `truncate` methods by using `scan` instead of `gsub` to
|
|
36
|
+
reduce allocation.
|
|
37
|
+
- Generalize the `capture:tables` Rake task to `capture:ansi` to regenerate
|
|
38
|
+
golden files for all `spec/assets/*.md` fixtures.
|
|
39
|
+
- Update `README.md` style examples to include `blockquote` and add a nested
|
|
40
|
+
dialog example.
|
|
41
|
+
|
|
42
|
+
### Build & CI
|
|
43
|
+
|
|
44
|
+
- Add `tins` gem (`~> 1.61`) as a runtime dependency.
|
|
45
|
+
- Upgrade `gem_hadar` development dependency to `>= 2.20.1`.
|
|
46
|
+
- Update `rubygems_version` in gemspec to **4.0.22**.
|
|
47
|
+
- Remove unused gems (`docile`, `simplecov-html`, `simplecov_json_formatter`)
|
|
48
|
+
from `.utilsrc`.
|
|
49
|
+
- Add `git config --global --add safe.directory /work` to Docker test script.
|
|
50
|
+
- Use `nproc` instead of `getconf _NPROCESSORS_ONLN` for CPU count in Docker
|
|
51
|
+
test script.
|
|
52
|
+
- Add `tmp/.gitkeep` to track the empty `tmp` directory.
|
|
53
|
+
|
|
54
|
+
### Testing
|
|
55
|
+
|
|
56
|
+
- Add `spec/kramdown/render_spec.rb` to test `Kramdown::ANSI.parse` for
|
|
57
|
+
headers, inline emphasis, code blocks, blockquotes, horizontal rules, lists,
|
|
58
|
+
links, images, and paragraphs.
|
|
59
|
+
- Add `spec/assets/tables.md` and `spec/assets/tables.ansi` golden files
|
|
60
|
+
covering 26 edge cases for table rendering.
|
|
61
|
+
- Add `spec/kramdown/ansi_golden_spec.rb` for output regression testing against
|
|
62
|
+
golden files.
|
|
63
|
+
- Add `spec/assets/html.md` and `spec/assets/html.ansi` golden files covering
|
|
64
|
+
HTML element styling.
|
|
65
|
+
- Add 9 new specs for `Kramdown::ANSI::Width#truncate` covering wide emojis,
|
|
66
|
+
empty strings, pure ANSI, zero-width characters, newline preservation,
|
|
67
|
+
multi-cell ellipsis, nested-style reset, and post-cut escape omission.
|
|
68
|
+
- Stub `Tins::Terminal.columns` to 80 in `ansi_spec.rb` and `render_spec.rb`
|
|
69
|
+
for deterministic output.
|
|
70
|
+
|
|
71
|
+
## 2026-03-21 v0.5.0
|
|
72
|
+
|
|
73
|
+
- Added `spec/kramdown/ansi_table_spec.rb` to the test suite and gemspec file
|
|
74
|
+
list.
|
|
75
|
+
- Updated `Kramdown::ANSI#convert_tr` to use column‑width ratios instead of a
|
|
76
|
+
fixed `full_width`.
|
|
77
|
+
- Refactored `Kramdown::ANSI::Width.wrap` to preserve ANSI escapes, clamp
|
|
78
|
+
lengths, and use a token‑based wrapping algorithm.
|
|
79
|
+
- Introduced `display_width` method in `Kramdown::ANSI::Width` to compute
|
|
80
|
+
visual width after stripping ANSI codes.
|
|
81
|
+
- Extracted ANSI escape removal logic into reusable
|
|
82
|
+
`Kramdown::ANSI::Width.remove_ansi_escapes`.
|
|
83
|
+
- Added lightweight `LoremIpsum` helper class in `spec/spec_helper.rb` for
|
|
84
|
+
deterministic dummy text.
|
|
85
|
+
- Updated `spec/kramdown/ansi_spec.rb` expectations to reflect new line‑break
|
|
86
|
+
behavior and added a direct `output.puts` call.
|
|
87
|
+
- Updated CI image configuration: install `build-base`, `yaml-dev`,
|
|
88
|
+
`openssl-dev`, `git`; removed `bundle update`; added before/after blocks;
|
|
89
|
+
removed `Gemfile.lock`.
|
|
90
|
+
- Added `.rspec` configuration with `--force-color` and exclusion of
|
|
91
|
+
`spec_helper`.
|
|
92
|
+
- Added `.utilsrc` configuration for util indexing, listing numerous gems and
|
|
93
|
+
test framework settings.
|
|
94
|
+
- Removed `require 'spec_helper'` from spec files, enabling colorized test
|
|
95
|
+
output.
|
|
96
|
+
- Added runtime dependency on `unicode-display_width` (~> **3.0**) for accurate
|
|
97
|
+
string width calculations.
|
|
98
|
+
|
|
3
99
|
## 2026-02-23 v0.4.1
|
|
4
100
|
|
|
5
101
|
- Updated the Ruby CI script to use `bundle exec` when running `rake spec`,
|
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:
|
|
51
|
-
strong:
|
|
52
|
-
em:
|
|
53
|
-
code:
|
|
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
|

|
|
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
|
@@ -36,8 +36,10 @@ GemHadar do
|
|
|
36
36
|
|
|
37
37
|
dependency 'term-ansicolor', '~> 1.11'
|
|
38
38
|
dependency 'kramdown-parser-gfm', '~> 1.1'
|
|
39
|
-
dependency 'terminal-table', '~>
|
|
39
|
+
dependency 'terminal-table', '~> 4.0'
|
|
40
|
+
dependency 'tins', '~> 1.61'
|
|
40
41
|
dependency 'json', '~> 2.0'
|
|
42
|
+
dependency 'unicode-display_width', '~> 3.0'
|
|
41
43
|
development_dependency 'all_images', '~> 0.4'
|
|
42
44
|
development_dependency 'rspec', '~> 3.2'
|
|
43
45
|
development_dependency 'debug'
|
|
@@ -47,3 +49,21 @@ GemHadar do
|
|
|
47
49
|
|
|
48
50
|
clobber 'coverage'
|
|
49
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
|
data/kramdown-ansi.gemspec
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
# stub: kramdown-ansi 0.
|
|
2
|
+
# stub: kramdown-ansi 0.6.0 ruby lib
|
|
3
3
|
|
|
4
4
|
Gem::Specification.new do |s|
|
|
5
5
|
s.name = "kramdown-ansi".freeze
|
|
6
|
-
s.version = "0.
|
|
6
|
+
s.version = "0.6.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,24 +13,26 @@ 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 = ["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/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, "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.
|
|
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/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.
|
|
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])
|
|
31
31
|
s.add_development_dependency(%q<simplecov>.freeze, [">= 0".freeze])
|
|
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
|
-
s.add_runtime_dependency(%q<terminal-table>.freeze, ["~>
|
|
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])
|
|
37
|
+
s.add_runtime_dependency(%q<unicode-display_width>.freeze, ["~> 3.0".freeze])
|
|
36
38
|
end
|
data/lib/kramdown/ansi/styles.rb
CHANGED
|
@@ -24,10 +24,15 @@ class Kramdown::ANSI::Styles
|
|
|
24
24
|
# configuration.
|
|
25
25
|
def initialize
|
|
26
26
|
@default_ansi_styles = {
|
|
27
|
-
header:
|
|
28
|
-
strong:
|
|
29
|
-
em:
|
|
30
|
-
code:
|
|
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
|
data/lib/kramdown/ansi/width.rb
CHANGED
|
@@ -19,6 +19,18 @@ module Kramdown::ANSI::Width
|
|
|
19
19
|
|
|
20
20
|
module_function
|
|
21
21
|
|
|
22
|
+
ANSI_ESCAPES = /\e\[.*?m|\e\].*?(\e|\a)\\?/
|
|
23
|
+
|
|
24
|
+
private_constant :ANSI_ESCAPES
|
|
25
|
+
|
|
26
|
+
# Removes ANSI escape sequences from the given string.
|
|
27
|
+
#
|
|
28
|
+
# @param line [String] the string potentially containing ANSI escape codes.
|
|
29
|
+
# @return [String] the string with all ANSI escape sequences removed.
|
|
30
|
+
def remove_ansi_escapes(line)
|
|
31
|
+
line.to_s.gsub(ANSI_ESCAPES, '')
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
# Returns the width of the terminal in characters, given a percentage.
|
|
23
35
|
#
|
|
24
36
|
# @param percentage [Numeric] the percentage value (defaults to 100.0)
|
|
@@ -27,26 +39,84 @@ module Kramdown::ANSI::Width
|
|
|
27
39
|
((Float(percentage) * Tins::Terminal.columns) / 100).floor
|
|
28
40
|
end
|
|
29
41
|
|
|
30
|
-
#
|
|
42
|
+
# Calculates the display width of a string after removing ANSI escape
|
|
43
|
+
# sequences.
|
|
44
|
+
#
|
|
45
|
+
# @param line [String] the string to measure
|
|
46
|
+
# @return [Integer] the number of columns the string occupies in a terminal
|
|
47
|
+
def display_width(line)
|
|
48
|
+
Unicode::DisplayWidth.of(remove_ansi_escapes(line))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Wraps a string to a given width, either by percentage of terminal
|
|
52
|
+
# width or by an explicit character length.
|
|
31
53
|
#
|
|
32
54
|
# @param text [String] the text to wrap
|
|
33
|
-
# @option percentage [Numeric] the percentage
|
|
34
|
-
# @option length [Integer] the character
|
|
35
|
-
# @return [String] the wrapped
|
|
55
|
+
# @option percentage [Numeric] the percentage of terminal width to use
|
|
56
|
+
# @option length [Integer] the explicit character width to wrap to
|
|
57
|
+
# @return [String] the wrapped string, preserving ANSI escape sequences
|
|
36
58
|
# @raise [ArgumentError] if neither `percentage` nor `length` is provided
|
|
59
|
+
# @example
|
|
60
|
+
# Kramdown::ANSI::Width.wrap("Hello world!", percentage: 50)
|
|
61
|
+
# # => "Hello\nworld!"
|
|
37
62
|
def wrap(text, percentage: nil, length: nil)
|
|
38
63
|
percentage.nil? ^ length.nil? or
|
|
39
64
|
raise ArgumentError, "either pass percentage or length argument"
|
|
40
65
|
percentage and length ||= width(percentage:)
|
|
41
|
-
text
|
|
42
|
-
|
|
43
|
-
|
|
66
|
+
return text if length <= 0
|
|
67
|
+
begin
|
|
68
|
+
length = length.to_i
|
|
69
|
+
rescue FloatDomainError
|
|
70
|
+
return text
|
|
71
|
+
end
|
|
72
|
+
length = length.clamp(1..)
|
|
73
|
+
length_without_ansi = 0
|
|
74
|
+
result = +''
|
|
75
|
+
text.scan(/(?<nws>\S+)|(?<ws>\s)/) do
|
|
76
|
+
token_value = $&
|
|
77
|
+
token_length = display_width(token_value)
|
|
78
|
+
if $~[:nws]
|
|
79
|
+
if length_without_ansi + token_length > length
|
|
80
|
+
length_without_ansi = token_length
|
|
81
|
+
if result[-1] == ' '
|
|
82
|
+
result[-1, 1] = "\n#{token_value}"
|
|
83
|
+
else
|
|
84
|
+
result << "\n#{token_value}"
|
|
85
|
+
end
|
|
86
|
+
else
|
|
87
|
+
length_without_ansi += token_length
|
|
88
|
+
result << token_value
|
|
89
|
+
end
|
|
44
90
|
else
|
|
45
|
-
|
|
91
|
+
length_without_ansi += token_length
|
|
92
|
+
result << token_value
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
result
|
|
96
|
+
end
|
|
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
|
|
46
114
|
end
|
|
47
|
-
end
|
|
115
|
+
end
|
|
48
116
|
end
|
|
49
117
|
|
|
118
|
+
private_constant :Token
|
|
119
|
+
|
|
50
120
|
# Truncates a given string to a specified length or percentage. If the text
|
|
51
121
|
# is longer an ellipsis sequence is added at the end of the generated string,
|
|
52
122
|
# to indicate that a truncation has been performed.
|
|
@@ -61,13 +131,59 @@ module Kramdown::ANSI::Width
|
|
|
61
131
|
percentage.nil? ^ length.nil? or
|
|
62
132
|
raise ArgumentError, "either pass percentage or length argument"
|
|
63
133
|
percentage and length ||= width(percentage:)
|
|
64
|
-
ellipsis_length = ellipsis
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
+
|
|
71
184
|
end
|
|
185
|
+
|
|
186
|
+
result << reset if found_esc && !result.end_with?(reset)
|
|
187
|
+
result
|
|
72
188
|
end
|
|
73
189
|
end
|
data/lib/kramdown/ansi.rb
CHANGED
|
@@ -56,7 +56,7 @@ class Kramdown::ANSI < Kramdown::Converter::Base
|
|
|
56
56
|
# @return [String] a copy of the input string with all ANSI escape
|
|
57
57
|
# sequences removed
|
|
58
58
|
def self.ansi_escape(line)
|
|
59
|
-
|
|
59
|
+
Kramdown::ANSI::Width.remove_ansi_escapes(line)
|
|
60
60
|
end
|
|
61
61
|
end
|
|
62
62
|
|
|
@@ -91,6 +91,35 @@ 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
|
+
source = source.gsub(
|
|
114
|
+
/(\|[^\n]*\n)([^|\n])/,
|
|
115
|
+
) do |match|
|
|
116
|
+
next_char = Regexp.last_match(2)
|
|
117
|
+
if next_char.match?(/\A\s*:?-+/)
|
|
118
|
+
match
|
|
119
|
+
else
|
|
120
|
+
"#{Regexp.last_match(1)}\n#{next_char}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
94
123
|
@doc = Kramdown::Document.new(source, options).to_ansi
|
|
95
124
|
end
|
|
96
125
|
|
|
@@ -253,6 +282,28 @@ class Kramdown::ANSI < Kramdown::Converter::Base
|
|
|
253
282
|
apply_style(:code) { el.value }
|
|
254
283
|
end
|
|
255
284
|
|
|
285
|
+
BLOCKQUOTE_QUOTES = [
|
|
286
|
+
%w[“ ”],
|
|
287
|
+
%w[‘ ’],
|
|
288
|
+
%w[ » « ],
|
|
289
|
+
%w[ › ‹ ],
|
|
290
|
+
].map(&:freeze).freeze
|
|
291
|
+
private_constant :BLOCKQUOTE_QUOTES
|
|
292
|
+
|
|
293
|
+
# Wraps the content of a blockquote level with the corresponding
|
|
294
|
+
# typographic quotation marks, cycling through a predefined set of
|
|
295
|
+
# quote pairs based on the nesting depth.
|
|
296
|
+
#
|
|
297
|
+
# @param level [ Integer ] the nesting level of the blockquote
|
|
298
|
+
# @yield [ String ] the inner content to be wrapped in quotation marks
|
|
299
|
+
# @return [ String ] the content enclosed by the appropriate opening and
|
|
300
|
+
# closing quote characters
|
|
301
|
+
def blockquote_quote(level, &block)
|
|
302
|
+
quote = BLOCKQUOTE_QUOTES[level % BLOCKQUOTE_QUOTES.size]
|
|
303
|
+
quote[0] + block.() + quote[1]
|
|
304
|
+
end
|
|
305
|
+
private :blockquote_quote
|
|
306
|
+
|
|
256
307
|
# The convert_blockquote method processes a blockquote element by applying
|
|
257
308
|
# proper formatting.
|
|
258
309
|
#
|
|
@@ -266,7 +317,19 @@ class Kramdown::ANSI < Kramdown::Converter::Base
|
|
|
266
317
|
# @return [String] the formatted blockquote content with quotation marks and
|
|
267
318
|
# proper newlines
|
|
268
319
|
def convert_blockquote(el, opts)
|
|
269
|
-
|
|
320
|
+
old_level = opts[:blockquote_level] ||= -1
|
|
321
|
+
level = opts[:blockquote_level] = old_level + 1
|
|
322
|
+
if level.zero?
|
|
323
|
+
newline(
|
|
324
|
+
blockquote_quote(level) {
|
|
325
|
+
apply_style(:blockquote) { inner(el, opts).strip }
|
|
326
|
+
}.gsub(/^/, ' ')
|
|
327
|
+
)
|
|
328
|
+
else
|
|
329
|
+
blockquote_quote(level) { inner(el, opts).strip }.gsub(/^/, ' ')
|
|
330
|
+
end
|
|
331
|
+
ensure
|
|
332
|
+
opts[:blockquote_level] = old_level
|
|
270
333
|
end
|
|
271
334
|
|
|
272
335
|
# The convert_hr method processes a horizontal rule element by generating a
|
|
@@ -373,6 +436,18 @@ class Kramdown::ANSI < Kramdown::Converter::Base
|
|
|
373
436
|
apply_style(:em) { inner(el, opts) }
|
|
374
437
|
elsif el.value == 'b' || el.value == 'strong'
|
|
375
438
|
apply_style(:strong) { inner(el, opts) }
|
|
439
|
+
elsif el.value == 'ins' || el.value == 'u'
|
|
440
|
+
apply_style(:ins) { inner(el, opts) }
|
|
441
|
+
elsif el.value == 'mark'
|
|
442
|
+
apply_style(:mark) { inner(el, opts) }
|
|
443
|
+
elsif el.value == 'del' || el.value == 's'
|
|
444
|
+
apply_style(:del) { inner(el, opts) }
|
|
445
|
+
elsif el.value == 'tt'
|
|
446
|
+
apply_style(:code) { inner(el, opts) }
|
|
447
|
+
elsif el.value == 'small'
|
|
448
|
+
apply_style(:small) { inner(el, opts) }
|
|
449
|
+
elsif %w[ span sub sup ].include?(el.value)
|
|
450
|
+
inner(el, opts)
|
|
376
451
|
else
|
|
377
452
|
''
|
|
378
453
|
end
|
|
@@ -457,14 +532,23 @@ class Kramdown::ANSI < Kramdown::Converter::Base
|
|
|
457
532
|
# addition of the row to the table instance
|
|
458
533
|
def convert_tr(el, opts)
|
|
459
534
|
return '' if el.children.empty?
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
535
|
+
cols = el.children.map do |c|
|
|
536
|
+
stripped = convert(c, opts).strip
|
|
537
|
+
stripped.empty? ? ' ' : stripped
|
|
538
|
+
end
|
|
539
|
+
return '' if cols.empty?
|
|
540
|
+
col_widths = cols.map { |c| display_width(c) }
|
|
541
|
+
col_widths_sum = col_widths.sum
|
|
542
|
+
full_width = width(percentage: 90)
|
|
543
|
+
ratios = cols.each_with_index.map do |c, i|
|
|
544
|
+
if col_widths_sum.zero?
|
|
545
|
+
1.0 / cols.size
|
|
546
|
+
else
|
|
547
|
+
col_widths[i] / col_widths_sum.to_f
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
wrapped_cols = cols.zip(ratios).map { |c, r| wrap(c, length: r * full_width) }
|
|
551
|
+
opts[:table] << wrapped_cols
|
|
468
552
|
''
|
|
469
553
|
end
|
|
470
554
|
|