markdown_exec 2.7.5 → 2.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0463a06b2bac6770c2443819a9cedee15e7aa3404649f077924074fd3c639ab
4
- data.tar.gz: 0f8b10556113950d468d92cfc11510178d7019d5b0e1c3f7ba0f61fdcab57414
3
+ metadata.gz: c36bb5c2d9e91eeeae59067f91fc88a3ad8e6b1a27ebfb474ff900903d030042
4
+ data.tar.gz: b002fc24e78006e830fdc66bb0be4df05fe9df62e6c036965eefbb7047f37ec7
5
5
  SHA512:
6
- metadata.gz: 32d0e93870011249641e929ab4c6963905321301b508831ef5b56f7be925f69857e0bab3b4f030cfe39f6a8dff83376a099bd568616df872b8d4b7291c2f5227
7
- data.tar.gz: aad2c863dd5bf6da344342e9336f6efa7045ec51d1471581b42bd26f42c1b1368d380b5f64781fb653154fd45f5f47db4d1fd7f22c8cc2f78c31a4781307b0dc
6
+ metadata.gz: 8eedac9012c2c0103afe99a8ea0cd235b26b3ab62618b35d79af6f685a56195818d28ba70588f93d754c76e5c4b3bb524de0ed09fd97864bdc79a424015911a9
7
+ data.tar.gz: d24b8c56e50416032de644f00d11f6bb81689617e47da57ec1e4b39a693c867dedff268ddd00e023bc134fc40ee9ae9b452a808131d129b378508464ac0793f5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.8.0] - 2025-02-10
4
+
5
+ ### Added
6
+
7
+ - Block type `ux` to facilitate the evaluation, display, and editing of shell variables.
8
+ The body of the `ux` block is in YAML.
9
+
10
+ A valid shell variable name is required as block key `name`. The remaining block keys are optional.
11
+
12
+ When the block is executed, its value is computed from the `default`, `exec`, `prompt`, `transform`, and `validate` keys and its output is assigned to the shell variable.
13
+
14
+ When a document is loaded, if one or more block names match `document_load_ux_block_name`, they are executed.
15
+ `ux_auto_load_force_default` limits the setting of the shell variable resulting from the execution of blocks executed at this time.
16
+
17
+ When an `ux` block is executed (after initial document load):
18
+ If the `default` value is the `exec` symbol, the command in the `exec` key is executed and its output is processed.
19
+ Else if the `allowed` value has one or more items, the user must pick from one of the items.
20
+ Else if the `prompt` value exists, the user must enter a value or nothing for the `default` value. The user is prompted with `prompt_ux_enter_a_value`.
21
+ Else the `default` value, as a string, is processed.
22
+
23
+ The output is validated/parsed by the regular expression in `validate`.
24
+ If the string matches, named groups are formatted with `menu_ux_row_format` and assigned to the shell variable.
25
+
26
+ The default `menu_ux_row_format` looks like a shell variable assignment.
27
+ If the output of `menu_ux_row_format` matches an immediately preceding table, the row is merged into that table.
28
+ Else the output is decorated with `menu_ux_color`.
29
+ - Improve final output of `test` Rake task so failures are visible.
30
+
3
31
  ## [2.7.5] - 2025-02-02
4
32
 
5
33
  ### Added
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- markdown_exec (2.7.5)
4
+ markdown_exec (2.8.0)
5
5
  clipboard (~> 1.3.6)
6
6
  open3 (~> 0.1.1)
7
7
  optparse (~> 0.1.1)
data/Rakefile CHANGED
@@ -155,9 +155,30 @@ end
155
155
 
156
156
  desc 'test'
157
157
  task :test do
158
- system 'bundle exec rspec'
158
+ success = true
159
+
160
+ # Run all tests and track failures
161
+ rspec_success = system('bundle exec rspec')
162
+ success = false unless rspec_success
163
+
159
164
  Rake::Task['minitest'].invoke
165
+ minitest_success = $?.success?
166
+ success = false unless minitest_success
167
+
160
168
  Rake::Task['bats'].invoke
169
+ bats_success = $?.success?
170
+ success = false unless bats_success
171
+
172
+ # Report failures and exit with non-zero status if any test failed
173
+ unless success
174
+ failed_tests = []
175
+ failed_tests << 'RSpec' unless rspec_success
176
+ failed_tests << 'Minitest' unless minitest_success
177
+ failed_tests << 'Bats' unless bats_success
178
+
179
+ puts "\nThe following test suites failed: #{failed_tests.join(', ')}"
180
+ exit 1
181
+ end
161
182
  end
162
183
 
163
184
  private
@@ -4,5 +4,5 @@ load 'test_helper'
4
4
 
5
5
  @test 'Command substitution' {
6
6
  spec_mde_xansi_dname_doc_blocks_expect docs/dev/command-substitution.md \
7
- 'CURRENT BASE NAME IS: MARKDOWN_EXEC_current base name is: markdown_exec_current base name is: markdown_exec_| current base name |_| ----------------- |_| markdown_exec |_: notice the string is not expanded in Shell block types (names or body)._ echo "current base name is now $(basename `pwd`)"__load: file_markdown_exec.sh'
7
+ 'CURRENT BASE NAME IS: MARKDOWN_EXEC_current base name is: markdown_exec_current base name is: markdown_exec_| current base name |_| ----------------- |_| markdown_exec |_: notice the string is not expanded in Shell block types (names or body)._ echo "current base name is now $(basename `pwd`)"__load: file_markdown_exec.sh_Status not zero: $(err)'
8
8
  }
@@ -4,5 +4,5 @@ load 'test_helper'
4
4
 
5
5
  @test 'Text and Headings' {
6
6
  spec_mde_xansi_dname_doc_blocks_expect docs/dev/line-wrapping.md \
7
- " DEMO WRAPPING LONG LINES__MDE detects the screen's dimensions:_height (lines) and width (characters)__Normal document text is displayed as_disabled menu lines. The width of these_lines is limited according to the screen's_width.__Test Indented Lines__ Indented with two spaces, this line_ should wrap in an aesthetically pleasing_ way.__ Indented with a tab, this line should_ wrap in an aesthetically pleasing way.__ SPECIES GENUS FAMILY ORDER CLASS PHYLUM_ KINGDOM DOMAIN_species genus family order class phylum_kingdom domain"
7
+ " DEMO WRAPPING LONG LINES__MDE detects the screen's dimensions:_height (lines) and width (characters)__Normal document text is displayed as_disabled menu lines. The width of these_lines is limited according to the_screen's width.__Test Indented Lines__ Indented with two spaces, this line_ should wrap in an aesthetically_ pleasing way.__ Indented with a tab, this line should_ wrap in an aesthetically pleasing_ way.__ SPECIES GENUS FAMILY ORDER CLASS PHYLUM_ KINGDOM DOMAIN_species genus family order class phylum_kingdom domain"
8
8
  }
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bats
2
+
3
+ load 'test_helper'
4
+
5
+ @test 'Tables - truncate columns' {
6
+ spec_mde_xansi_dname_doc_blocks_expect docs/dev/table-column-truncate.md \
7
+ 'DEMONSTRATE TRUNCATION OF TEXT IN TABLE CELLS__| Common Name | Species | Genus | Family | Year Di |_| ------------ | ------------ | ----- | ------- | ------- |_| Tapanuli Ora | Pongo tapanu | Pongo | Hominid | 2017 |_| Psychedelic | Histiophryne | Histi | Antenna | 2009 |_| Ruby Seadrag | Phyllopteryx | Phyll | Syngnat | 2015 |_| Illacme tobi | Illacme tobi | Illac | Siphono | 2016 |_| Spiny Dandel | Taraxacum ja | Tarax | Asterac | 2022 |__'
8
+ }
data/bats/table.bats CHANGED
@@ -4,7 +4,7 @@ load 'test_helper'
4
4
 
5
5
  @test 'Tables - indented' {
6
6
  spec_mde_xansi_dname_doc_blocks_expect docs/dev/table-indent.md \
7
- 'DEMONSTRATE TABLE INDENTATION__Table flush at left._Centered columns._| Common Name | Species | Genus | Family | Year Discovered |_| -------------------- | ------------------------- | ------------ | ------------- | --------------- |_| Tapanuli Orangutan | Pongo tapanuliensis | Pongo | Hominidae | 2017 |_| Psychedelic Frogfish | Histiophryne psychedelica | Histiophryne | Antennariidae | 2009 |_| Ruby Seadragon | Phyllopteryx dewysea | Phyllopteryx | Syngnathidae | 2015 |__ Table indented with two spaces._ Left-justified columns._ | Common Name | Species | Genus | Family | Year Discovered |_ | -------------------------- | -------------- | ------- | --------------- | --------------- |_ | Illacme tobini (Millipede) | Illacme tobini | Illacme | Siphonorhinidae | 2016 |__ Table indented with one tab._ Right-justified columns._ | Common Name | Species | Genus | Family | Year Discovered |_ | --------------- | ------------------- | --------- | ---------- | --------------- |_ | Spiny Dandelion | Taraxacum japonicum | Taraxacum | Asteraceae | 2022 |'
7
+ 'DEMONSTRATE TABLE INDENTATION__Table flush at left._Centered columns._| Common Name | Species | Genus | Family | Year Discover |_| ------------------ | ---------------------- | ---------- | ----------- | ------------- |_| Tapanuli Orangutan | Pongo tapanuliensis | Pongo | Hominidae | 2017 |_| Psychedelic Frogfi | Histiophryne psychedel | Histiophry | Antennariid | 2009 |_| Ruby Seadragon | Phyllopteryx dewysea | Phyllopter | Syngnathida | 2015 |__ Table indented with two spaces._ Left-justified columns._ | Common Name | Species | Genus | Family | Year Discovere |_ | ------------------------- | ------------- | ------ | -------------- | -------------- |_ | Illacme tobini (Millipede | Illacme tobin | Illacm | Siphonorhinida | 2016 |__ Table indented with one tab._ Right-justified columns._ | Common Name | Species | Genus | Family | Year Discovered |_ | --------------- | ------------------- | --------- | ---------- | --------------- |_ | Spiny Dandelion | Taraxacum japonicum | Taraxacum | Asteraceae | 2022 |'
8
8
  }
9
9
 
10
10
  @test 'Tables - invalid' {
@@ -13,7 +13,7 @@ __filedirs_all()
13
13
  }
14
14
 
15
15
  _mde_echo_version() {
16
- echo "2.7.5"
16
+ echo "2.8.0"
17
17
  }
18
18
 
19
19
  _mde() {
@@ -10,6 +10,9 @@ dump_selected_block: false # Dump selected block
10
10
  execute_in_own_window: false
11
11
 
12
12
  menu_divider_format:
13
+
14
+ menu_for_saved_lines: false
15
+
13
16
  menu_with_back: false
14
17
  menu_with_exit: false
15
18
  menu_with_shell: false
@@ -11,6 +11,7 @@ echo "current base name is now $(basename `pwd`)"
11
11
  ```link
12
12
  load: file_$(basename `pwd`).sh
13
13
  ```
14
+ Status not zero: $(err)
14
15
  / This should not be evaluated $(err). It errs with "Error: HashDelegator.blocks_from_nested_files -- Shell script execution failed: /bin/bash: line 2: err: command not found"
15
16
  @import bats-document-configuration.md
16
17
  ```opts :(document_opts)
@@ -0,0 +1,17 @@
1
+ # Demonstrate Truncation of Text in Table Cells
2
+
3
+ | Common Name| Species| Genus| Family| Year Discovered
4
+ | -| -|:-|-:|:-:
5
+ | Tapanuli Orangutan| Pongo tapanuliensis| Pongo| Hominidae| 2017
6
+ | Psychedelic Frogfish| Histiophryne psychedelica| Histiophryne| Antennariidae| 2009
7
+ | Ruby Seadragon| Phyllopteryx dewysea| Phyllopteryx| Syngnathidae| 2015
8
+ | Illacme tobini (Millipede)| Illacme tobini| Illacme| Siphonorhinidae| 2016
9
+ | Spiny Dandelion| Taraxacum japonicum| Taraxacum| Asteraceae| 2022
10
+ ```
11
+ ```
12
+ @import bats-document-configuration.md
13
+ ```opts :(document_opts)
14
+ heading1_center: false
15
+ screen_width: 60
16
+ table_center: false
17
+ ```
@@ -22,5 +22,6 @@ Centered columns.
22
22
  @import bats-document-configuration.md
23
23
  ```opts :(document_opts)
24
24
  heading1_center: false
25
+ screen_width: 100
25
26
  table_center: false
26
27
  ```
data/examples/colors.md CHANGED
@@ -33,6 +33,7 @@ menu_opts_color: fg_rgbh_1f_00_1f
33
33
  menu_opts_set_color: fg_rgbh_7f_00_1f
34
34
  menu_save_color: fg_rgbh_e0_e0_20
35
35
  menu_task_color: fg_rgbh_1f_1f_1f
36
+ menu_ux_color: fg_rgbh_2f_c0_2f
36
37
  menu_vars_color: fg_rgbh_1f_a0_1f
37
38
  menu_vars_set_color: fg_rgbh_00_1f_1f
38
39
  output_execution_label_name_color: fg_rgbh_00_1f_00
@@ -60,6 +61,7 @@ menu_opts_set_color: fg_rgbh_7f_00_ff
60
61
  # menu_save_color: fg_rgbh_ea_ea_20
61
62
  menu_save_color: fg_rgbh_ff_ff_20
62
63
  menu_task_color: fg_rgbh_ff_ff_ff
64
+ menu_ux_color: fg_rgbh_df_c0_df
63
65
  menu_vars_color: fg_rgbh_ff_a0_ff
64
66
  menu_vars_set_color: fg_rgbh_00_ff_ff
65
67
  output_execution_label_name_color: fg_rgbh_00_ff_00
data/lib/block_types.rb CHANGED
@@ -14,6 +14,7 @@ class BlockType
14
14
  SAVE = 'save',
15
15
  SHELL = 'shell',
16
16
  TEXT = 'text',
17
+ UX = 'ux',
17
18
  VARS = 'vars',
18
19
  VIEW = 'view',
19
20
  YAML = 'yaml'
data/lib/constants.rb CHANGED
@@ -27,12 +27,12 @@ BLOCK_TYPE_COLOR_OPTIONS = {
27
27
  BlockType::OPTS => :menu_opts_color,
28
28
  BlockType::SAVE => :menu_save_color,
29
29
  BlockType::SHELL => :menu_bash_color,
30
+ BlockType::UX => :menu_ux_color,
30
31
  BlockType::VARS => :menu_vars_color
31
32
  }.freeze
32
33
 
33
-
34
34
  COLLAPSIBLE_SYMBOL_COLLAPSED = '⬢' # '<+>' # '∆'
35
- COLLAPSIBLE_SYMBOL_EXPANDED = '⬡' # '< >' # '…'
35
+ COLLAPSIBLE_SYMBOL_EXPANDED = '⬡' # '< >' # '…'
36
36
 
37
37
  # in regexp (?<collapse>[+-~]?)
38
38
  COLLAPSIBLE_TOKEN_COLLAPSE = '+'
@@ -5,8 +5,12 @@
5
5
 
6
6
  require 'open3'
7
7
 
8
+ class EvaluateShellExpression
9
+ StatusFail = :script_execution_failed unless const_defined?(:StatusFail)
10
+ end
11
+
8
12
  def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
9
- key_format: "%%<%s>",
13
+ key_format: '%%<%s>',
10
14
  initial_code_required: false)
11
15
  # !!p initial_code expressions key_format shell
12
16
  return if (initial_code_required && (initial_code.nil? || initial_code.empty?)) ||
@@ -18,23 +22,26 @@ def evaluate_shell_expressions(initial_code, expressions, shell: '/bin/bash',
18
22
 
19
23
  # Construct a single shell script
20
24
  script = initial_code.dup
21
- expressions.each_with_index do |(key, expression), index|
25
+ expressions.each_with_index do |(_key, expression), index|
22
26
  script << "\necho #{token}#{index}\n"
23
27
  script << expression << "\n"
24
28
  end
25
29
 
26
30
  # Execute
27
- stdout_str, stderr_str, status = Open3.capture3(shell, "-c", script)
31
+ stdout_str, stderr_str, status = Open3.capture3(shell, '-c', script)
28
32
 
29
33
  unless status.success?
30
- raise "Shell script execution failed: #{stderr_str}"
34
+ return EvaluateShellExpression::StatusFail
31
35
  end
32
36
 
33
37
  # Extract output for expressions
34
38
  result_hash = {}
35
- stdout_str.split(/\n?#{token}\d+\n/)[1..-1].tap do |output_parts|
36
- expressions.each_with_index do |(key, _expression), index|
37
- result_hash[sprintf(key_format, key)] = output_parts[index].chomp
39
+ part = stdout_str.split(/\n?#{token}\d+\n/)
40
+ unless part.empty?
41
+ part[1..-1].tap do |output_parts|
42
+ expressions.each_with_index do |(key, _expression), index|
43
+ result_hash[format(key_format, key)] = output_parts[index].chomp
44
+ end
38
45
  end
39
46
  end
40
47
 
@@ -58,23 +65,23 @@ class TestShellExpressionEvaluator < Minitest::Test
58
65
  end
59
66
 
60
67
  def test_single_expression
61
- expressions = { "greeting" => "echo 'Hello, World!'" }
68
+ expressions = { 'greeting' => "echo 'Hello, World!'" }
62
69
  result = evaluate_shell_expressions(@initial_code, expressions)
63
70
 
64
- assert_equal "Hello, World!", result["%<greeting>"]
71
+ assert_equal 'Hello, World!', result['%<greeting>']
65
72
  end
66
73
 
67
74
  def test_multiple_expressions
68
75
  expressions = {
69
- "greeting" => "echo 'Hello, World!'",
70
- "date" => "date +%Y-%m-%d",
71
- "kernel" => "uname -r"
76
+ 'greeting' => "echo 'Hello, World!'",
77
+ 'date' => 'date +%Y-%m-%d',
78
+ 'kernel' => 'uname -r'
72
79
  }
73
80
  result = evaluate_shell_expressions(@initial_code, expressions)
74
81
 
75
- assert_equal "Hello, World!", result["%<greeting>"]
76
- assert_match /\d{4}-\d{2}-\d{2}/, result["%<date>"]
77
- assert_match /\d+\.\d+\.\d+/, result["%<kernel>"]
82
+ assert_equal 'Hello, World!', result['%<greeting>']
83
+ assert_match(/\d{4}-\d{2}-\d{2}/, result['%<date>'])
84
+ assert_match(/\d+\.\d+\.\d+/, result['%<kernel>'])
78
85
  end
79
86
 
80
87
  def test_empty_expressions_list
@@ -85,13 +92,11 @@ class TestShellExpressionEvaluator < Minitest::Test
85
92
  end
86
93
 
87
94
  def test_invalid_expression
88
- expressions = { "invalid" => "invalid_command" }
95
+ expressions = { 'invalid' => 'invalid_command' }
89
96
 
90
- error = assert_raises(RuntimeError) do
91
- evaluate_shell_expressions(@initial_code, expressions)
92
- end
97
+ result = evaluate_shell_expressions(@initial_code, expressions)
93
98
 
94
- assert_match /Shell script execution failed/, error.message
99
+ assert_equal EvaluateShellExpression::StatusFail, result
95
100
  end
96
101
 
97
102
  def test_initial_code_execution
@@ -99,17 +104,17 @@ class TestShellExpressionEvaluator < Minitest::Test
99
104
  #!/bin/sh
100
105
  echo "Custom setup message"
101
106
  BASH
102
- expressions = { "test" => "echo Test after initial setup" }
107
+ expressions = { 'test' => 'echo Test after initial setup' }
103
108
 
104
109
  result = evaluate_shell_expressions(initial_code, expressions)
105
110
 
106
- assert_equal "Test after initial setup", result["%<test>"]
111
+ assert_equal 'Test after initial setup', result['%<test>']
107
112
  end
108
113
 
109
114
  def test_large_number_of_expressions
110
- expressions = (1..100).map { |i|
115
+ expressions = (1..100).map do |i|
111
116
  ["expr_#{i}", "echo Expression #{i}"]
112
- }.to_h
117
+ end.to_h
113
118
 
114
119
  result = evaluate_shell_expressions(@initial_code, expressions)
115
120
 
data/lib/fcb.rb CHANGED
@@ -5,6 +5,29 @@
5
5
  require 'digest'
6
6
  require_relative 'namer'
7
7
 
8
+ def parse_yaml_of_ux_block(
9
+ data,
10
+ menu_format: nil,
11
+ prompt: nil,
12
+ validate: nil
13
+ )
14
+ export = data['export']
15
+ export = data if export.nil?
16
+ name = export['name']
17
+ raise "Invalid data: #{data.inspect}" unless name.present?
18
+
19
+ OpenStruct.new(
20
+ allowed: export['allowed'],
21
+ default: export['default'],
22
+ exec: export['exec'],
23
+ menu_format: export['menu_format'] || menu_format,
24
+ name: name,
25
+ prompt: export['prompt'] || prompt,
26
+ transform: export['transform'],
27
+ validate: export['validate'] || validate
28
+ )
29
+ end
30
+
8
31
  module MarkdownExec
9
32
  class Error < StandardError; end
10
33
 
@@ -106,12 +129,14 @@ module MarkdownExec
106
129
  # @return [Object] The modified functional code block with updated
107
130
  # summary attributes.
108
131
  def for_menu!(
109
- block_calls_scan: @delegate_object[:block_calls_scan],
110
- block_name_match: @delegate_object[:block_name_match],
111
- block_name_nick_match: @delegate_object[:block_name_nick_match],
112
- id: ''
132
+ block_calls_scan:,
133
+ block_name_match:,
134
+ block_name_nick_match:,
135
+ id: '',
136
+ menu_format:,
137
+ prompt:,
138
+ table_center:
113
139
  )
114
- # binding.irb
115
140
  call = @attrs[:call] = @attrs[:start_line]&.match(
116
141
  Regexp.new(block_calls_scan)
117
142
  )&.fetch(1, nil)
@@ -130,6 +155,23 @@ module MarkdownExec
130
155
  end
131
156
  @attrs[:title] = @attrs[:oname] = oname
132
157
  @attrs[:id] = id
158
+
159
+ if @attrs[:type] == BlockType::UX
160
+ case data = YAML.load(@attrs[:body].join("\n"))
161
+ when Hash
162
+ export = parse_yaml_of_ux_block(
163
+ data,
164
+ menu_format: menu_format,
165
+ prompt: prompt
166
+ )
167
+
168
+ @attrs[:center] = table_center
169
+ oname = @attrs[:oname] = format(export.menu_format, export.to_h)
170
+ else
171
+ raise "Invalid data type: #{data.inspect}"
172
+ end
173
+ end
174
+
133
175
  @attrs[:dname] = HashDelegator.indent_all_lines(
134
176
  (yield oname, BLOCK_TYPE_COLOR_OPTIONS[@attrs[:type]]),
135
177
  @attrs[:indent]
data/lib/format_table.rb CHANGED
@@ -65,12 +65,15 @@ module MarkdownTableFormatter
65
65
 
66
66
  def format_cell(cell, align, width, truncate: true)
67
67
  plain_string = cell.gsub(/\033\[[\d;]+m|\033\[0m/, '')
68
+ exceeded = plain_string.length > width
68
69
  truncated = false
69
70
  ret = TrackedString.new(
70
71
  case
71
- when truncate && plain_string.length > width
72
+ when truncate && exceeded
72
73
  truncated = true
73
- plain_string[0, width]
74
+ plain_string[0, width] ### s/trim decorated text
75
+ when exceeded
76
+ cell
74
77
  when align == :center
75
78
  cell.center(width)
76
79
  when align == :right
@@ -79,6 +82,7 @@ module MarkdownTableFormatter
79
82
  cell.ljust(width)
80
83
  end
81
84
  )
85
+ ret.exceeded = exceeded
82
86
  ret.truncated = truncated
83
87
  ret
84
88
  end
@@ -152,11 +156,8 @@ module MarkdownTableFormatter
152
156
  alignment_indicators, column_widths =
153
157
  calculate_column_alignment_and_widths(rows, column_count)
154
158
 
155
- # ww0 'table_width', table_width
156
- # ww0 'truncate', truncate
157
-
158
159
  unless table_width.nil?
159
- sum_column_widths = column_widths.sum
160
+ sum_column_widths = column_widths.sum + ((column_count * 3) + 5)
160
161
  if sum_column_widths > table_width
161
162
  ratio = table_width.to_f / sum_column_widths
162
163
  column_widths.each_with_index do |width, i|
@@ -168,7 +169,7 @@ module MarkdownTableFormatter
168
169
  format_rows__hs(
169
170
  rows, alignment_indicators, column_widths, decorate,
170
171
  truncate: truncate
171
- ).tap { |ret| binding.irb if ret.class == TrackedString }
172
+ )
172
173
  end
173
174
 
174
175
  def insert_every_other(array, obj)
@@ -92,6 +92,10 @@ module HashDelegatorSelf
92
92
  blocks.find { |item| item.send(msg) == value } || default
93
93
  end
94
94
 
95
+ def block_match(blocks, msg, value, default = nil)
96
+ blocks.select { |item| value =~ item.send(msg) }
97
+ end
98
+
95
99
  def block_select(blocks, msg, value, default = nil)
96
100
  blocks.select { |item| item.send(msg) == value }
97
101
  end
@@ -297,14 +301,16 @@ module HashDelegatorSelf
297
301
  truncate: $table_cell_truncate
298
302
  )
299
303
 
300
- truncated_table_cell = false
304
+ exceeded_table_cell = false # any cell in table is exceeded
305
+ truncated_table_cell = false # any cell in table is truncated
301
306
  table__hs.each do |table_hs|
302
307
  table_hs.substrings.each do |substrings|
303
308
  substrings.each do |node|
304
- if node[:text].class == TrackedString
305
- truncated_table_cell = node[:text].truncated
306
- break if truncated_table_cell
307
- end
309
+ next unless node[:text].class == TrackedString
310
+
311
+ exceeded_table_cell ||= node[:text].exceeded
312
+ truncated_table_cell = node[:text].truncated
313
+ break if truncated_table_cell
308
314
  end
309
315
  break if truncated_table_cell
310
316
  end
@@ -337,9 +343,9 @@ module HashDelegatorSelf
337
343
 
338
344
  if ind.zero?
339
345
  fcb.truncated_table_cell = truncated_table_cell
340
- # if truncated_table_cell
341
- fcb.delete_key(:disabled)
342
- # end
346
+ if exceeded_table_cell
347
+ fcb.delete_key(:disabled)
348
+ end
343
349
  end
344
350
  end
345
351
  end
@@ -588,6 +594,7 @@ module MarkdownExec
588
594
  @prompt = HashDelegator.tty_prompt_without_disabled_symbol
589
595
 
590
596
  @opts_most_recent_filename = nil
597
+ @ux_most_recent_filename = nil
591
598
  @vars_most_recent_filename = nil
592
599
  @pass_args = []
593
600
  @run_state = OpenStruct.new(
@@ -865,7 +872,6 @@ module MarkdownExec
865
872
  blocks = []
866
873
  iter_blocks_from_nested_files do |btype, fcb|
867
874
  count += 1
868
-
869
875
  case btype
870
876
  when :blocks
871
877
  if @delegate_object[:bash]
@@ -873,7 +879,10 @@ module MarkdownExec
873
879
  block_calls_scan: @delegate_object[:block_calls_scan],
874
880
  block_name_match: @delegate_object[:block_name_match],
875
881
  block_name_nick_match: @delegate_object[:block_name_nick_match],
876
- id: "#{source_id}_bfnf_b_#{count}"
882
+ id: "#{source_id}_bfnf_b_#{count}",
883
+ menu_format: @delegate_object[:menu_ux_row_format],
884
+ prompt: @delegate_object[:prompt_ux_enter_a_value],
885
+ table_center: @delegate_object[:table_center]
877
886
  ) do |oname, color|
878
887
  apply_block_type_color_option(oname, color)
879
888
  end
@@ -935,7 +944,7 @@ module MarkdownExec
935
944
  (link_state&.inherited_lines_block || ''), commands,
936
945
  initial_code_required: initial_code_required,
937
946
  key_format: key_format
938
- ) # !!t
947
+ )
939
948
  end
940
949
 
941
950
  def calc_logged_stdout_filename(block_name:)
@@ -1008,6 +1017,135 @@ module MarkdownExec
1008
1017
  ]
1009
1018
  end
1010
1019
 
1020
+ def neval(export_exec, inherited_code)
1021
+ # ww0 export_exec
1022
+ # ww0 inherited_code
1023
+ code = (inherited_code || []) + [export_exec]
1024
+ filespec = generate_temp_filename
1025
+ File.write filespec, HashDelegator.join_code_lines(code)
1026
+ File.chmod 0o755, filespec
1027
+ # ww0 File.read(filespec)
1028
+ ret = `#{filespec}`
1029
+ File.delete filespec
1030
+ ret
1031
+ end
1032
+
1033
+ # sets ENV
1034
+ def code_from_ux_block_to_set_environment_variables(
1035
+ selected, mdoc, inherited_code: nil, force: true, only_default: false
1036
+ )
1037
+ # ww0 inherited_code
1038
+ # ww0 mdoc
1039
+ exit_prompt = @delegate_object[:prompt_filespec_back]
1040
+
1041
+ required = mdoc.collect_recursively_required_code(
1042
+ anyname: selected.pub_name,
1043
+ label_format_above: @delegate_object[:shell_code_label_format_above],
1044
+ label_format_below: @delegate_object[:shell_code_label_format_below],
1045
+ block_source: block_source
1046
+ )
1047
+
1048
+ code_lines = []
1049
+ case data = YAML.load(selected.body.join("\n"))
1050
+ when Hash
1051
+ export = parse_yaml_of_ux_block(
1052
+ data,
1053
+ prompt: @delegate_object[:prompt_ux_enter_a_value],
1054
+ validate: '^(?<name>[^ ].*)$'
1055
+ )
1056
+
1057
+ exportable = true
1058
+ if only_default
1059
+ value = case export.default
1060
+ # exec > default
1061
+ when :exec
1062
+ raise unless export.exec.present?
1063
+
1064
+ n1 = neval(export.exec, (inherited_code || []) + required[:code])
1065
+
1066
+ if export.transform.present?
1067
+ if export.transform.is_a? Symbol
1068
+ n1.send(export.transform)
1069
+ else
1070
+ format(
1071
+ export.transform,
1072
+ NamedCaptureExtractor.extract_named_groups(
1073
+ n1, export.validate
1074
+ )
1075
+ )
1076
+ end
1077
+ else
1078
+ n1
1079
+ end
1080
+
1081
+ # default
1082
+ else
1083
+ export.default.to_s
1084
+ end
1085
+ else
1086
+ caps = nil
1087
+ value = nil
1088
+
1089
+ # exec > allowed
1090
+ if export.exec
1091
+ value = neval(export.exec, (inherited_code || []) + required[:code])
1092
+ caps = NamedCaptureExtractor.extract_named_groups(value,
1093
+ export.validate)
1094
+
1095
+ # allowed > prompt
1096
+ elsif export.allowed && export.allowed.count.positive?
1097
+ case (choice = prompt_select_code_filename(
1098
+ [exit_prompt] + export.allowed,
1099
+ string: export.prompt,
1100
+ color_sym: :prompt_color_after_script_execution
1101
+ ))
1102
+ when exit_prompt
1103
+ exportable = false
1104
+ else
1105
+ value = choice
1106
+ caps = NamedCaptureExtractor.extract_named_groups(value,
1107
+ export.validate)
1108
+ end
1109
+
1110
+ # prompt > default
1111
+ elsif export.prompt.present?
1112
+ begin
1113
+ while true
1114
+ print "#{export.prompt} [#{export.default}]: "
1115
+ value = gets.chomp
1116
+ value = export.default.to_s if value.empty?
1117
+ caps = NamedCaptureExtractor.extract_named_groups(value,
1118
+ export.validate)
1119
+ break if caps
1120
+
1121
+ # invalid input, retry
1122
+
1123
+ end
1124
+ rescue Interrupt
1125
+ exportable = false
1126
+ end
1127
+
1128
+ # default
1129
+ else
1130
+ value = export.default
1131
+ end
1132
+
1133
+ if exportable && export.transform.present?
1134
+ value = format(export.transform, caps)
1135
+ end
1136
+ end
1137
+
1138
+ if exportable
1139
+ ENV[export.name] = value.to_s
1140
+ code_lines.push code_line_safe_assign(export.name, value,
1141
+ force: force)
1142
+ end
1143
+ else
1144
+ raise "Invalid data type: #{data.inspect}"
1145
+ end
1146
+ code_lines
1147
+ end
1148
+
1011
1149
  # sets ENV
1012
1150
  def code_from_vars_block_to_set_environment_variables(selected)
1013
1151
  code_lines = []
@@ -1027,6 +1165,15 @@ module MarkdownExec
1027
1165
  code_lines
1028
1166
  end
1029
1167
 
1168
+ def code_line_safe_assign(name, value, force:)
1169
+ if force
1170
+ # ww0 value
1171
+ "#{name}=#{Shellwords.escape(value)}"
1172
+ else
1173
+ "[[ -z $#{name} ]] && #{name}=#{Shellwords.escape(value)}"
1174
+ end
1175
+ end
1176
+
1030
1177
  def collect_line_decor_patterns(delegate_object)
1031
1178
  extract_patterns = lambda do |key|
1032
1179
  return [] unless delegate_object[key].present?
@@ -1412,7 +1559,7 @@ module MarkdownExec
1412
1559
  @delegate_object[criteria[:color]].to_sym,
1413
1560
  decor_patterns:
1414
1561
  @decor_patterns_from_delegate_object_for_block_create,
1415
- disabled: fcb.truncated_table_cell.nil? && !(criteria[:collapsible] && @delegate_object[criteria[:collapsible]]),
1562
+ disabled: !(criteria[:collapsible] && @delegate_object[criteria[:collapsible]]),
1416
1563
  fcb: fcb,
1417
1564
  id: "#{id}.#{index}",
1418
1565
  format_option: criteria[:format] &&
@@ -1790,6 +1937,18 @@ module MarkdownExec
1790
1937
  )
1791
1938
  next_state_set_code(selected, link_state, required_lines)
1792
1939
 
1940
+ elsif selected.type == BlockType::UX
1941
+ debounce_reset
1942
+ next_state_append_code(
1943
+ selected,
1944
+ link_state,
1945
+ code_from_ux_block_to_set_environment_variables(
1946
+ selected,
1947
+ @dml_mdoc,
1948
+ inherited_code: @dml_link_state.inherited_lines
1949
+ )
1950
+ )
1951
+
1793
1952
  elsif selected.type == BlockType::VARS
1794
1953
  debounce_reset
1795
1954
  next_state_append_code(selected, link_state,
@@ -2296,7 +2455,7 @@ module MarkdownExec
2296
2455
 
2297
2456
  variable_counts = count_named_group_occurrences(blocks, pattern,
2298
2457
  group_name: group_name)
2299
- return if variable_counts.nil?
2458
+ return if variable_counts.nil? || variable_counts == {}
2300
2459
 
2301
2460
  echo_commands = generate_echo_commands(variable_counts, echo_format)
2302
2461
 
@@ -2307,6 +2466,7 @@ module MarkdownExec
2307
2466
  )
2308
2467
 
2309
2468
  return if replacements.nil?
2469
+ return if replacements == EvaluateShellExpression::StatusFail
2310
2470
 
2311
2471
  expand_blocks_with_replacements(blocks, replacements)
2312
2472
  end
@@ -2723,6 +2883,37 @@ module MarkdownExec
2723
2883
  true
2724
2884
  end
2725
2885
 
2886
+ def load_auto_ux_block(
2887
+ all_blocks,
2888
+ mdoc,
2889
+ block_name: @delegate_object[:document_load_ux_block_name]
2890
+ )
2891
+ unless block_name.present? &&
2892
+ @ux_most_recent_filename != @delegate_object[:filename]
2893
+ return
2894
+ end
2895
+
2896
+ blocks = HashDelegator.block_select(all_blocks, :oname, block_name)
2897
+ if blocks.empty?
2898
+ blocks = HashDelegator.block_match(all_blocks, :nickname,
2899
+ Regexp.new(block_name))
2900
+ end
2901
+ return if blocks.empty?
2902
+
2903
+ @ux_most_recent_filename = @delegate_object[:filename]
2904
+
2905
+ (blocks.each.with_object([]) do |block, merged_options|
2906
+ merged_options.push(
2907
+ code_from_ux_block_to_set_environment_variables(
2908
+ block,
2909
+ mdoc,
2910
+ force: @delegate_object[:ux_auto_load_force_default],
2911
+ only_default: true
2912
+ )
2913
+ )
2914
+ end).to_a
2915
+ end
2916
+
2726
2917
  def load_auto_vars_block(all_blocks,
2727
2918
  block_name: @delegate_object[:document_load_vars_block_name])
2728
2919
  unless block_name.present? &&
@@ -2871,6 +3062,16 @@ module MarkdownExec
2871
3062
  reload_blocks = true
2872
3063
  end
2873
3064
 
3065
+ # load document ux block
3066
+ #
3067
+ if code_lines = load_auto_ux_block(all_blocks, mdoc)
3068
+ new_code = HashDelegator.code_merge(link_state.inherited_lines,
3069
+ code_lines)
3070
+ next_state_set_code(nil, link_state, new_code)
3071
+ link_state.inherited_lines = new_code
3072
+ reload_blocks = true
3073
+ end
3074
+
2874
3075
  # load document vars block
2875
3076
  #
2876
3077
  if code_lines = load_auto_vars_block(all_blocks)
@@ -2924,7 +3125,6 @@ module MarkdownExec
2924
3125
  HashDelegator.tables_into_columns!(menu_blocks, @delegate_object,
2925
3126
  screen_width_for_table)
2926
3127
 
2927
-
2928
3128
  [all_blocks, menu_blocks, mdoc]
2929
3129
  end
2930
3130
 
@@ -2988,7 +3188,6 @@ module MarkdownExec
2988
3188
  def menu_toggle_collapsible_block(selected)
2989
3189
  # return true if @compress_ids.key?(fcb.id) && !!@compress_ids[fcb.id]
2990
3190
  # return false if @expand_ids.key?(fcb.id) && !!@expand_ids[fcb.id]
2991
- # binding.irb
2992
3191
  if @compressed_ids.key?(selected.id) && !!@compressed_ids[selected.id]
2993
3192
  @compressed_ids.delete(selected.id)
2994
3193
  @expanded_ids[selected.id] = selected.level
@@ -3705,15 +3904,17 @@ module MarkdownExec
3705
3904
  end
3706
3905
 
3707
3906
  def screen_width
3708
- if @delegate_object[:screen_width] && @delegate_object[:screen_width].positive?
3709
- @delegate_object[:screen_width]
3907
+ width = @delegate_object[:screen_width]
3908
+ if width && width.positive?
3909
+ width
3710
3910
  else
3711
3911
  @delegate_object[:console_width]
3712
3912
  end
3713
3913
  end
3714
3914
 
3715
3915
  def screen_width_for_table
3716
- screen_width - prompt_margin_left_width - prompt_margin_right_width - 2 # prompt is symbol + space (width: 2)
3916
+ # menu adds newline after some lines if sized to the edge
3917
+ screen_width - prompt_margin_left_width - prompt_margin_right_width - 3 # menu prompt symbol (1) + space (1) + gap (1)
3717
3918
  end
3718
3919
 
3719
3920
  def screen_width_for_wrapping
@@ -3,10 +3,11 @@
3
3
  require_relative 'ansi_string'
4
4
 
5
5
  class TrackedString < String
6
- attr_accessor :truncated
6
+ attr_accessor :exceeded, :truncated
7
7
 
8
8
  def initialize(str)
9
- super(str)
9
+ super
10
+ @exceeded = false
10
11
  @truncated = false
11
12
  end
12
13
  end
@@ -7,5 +7,5 @@ module MarkdownExec
7
7
  BIN_NAME = 'mde'
8
8
  GEM_NAME = 'markdown_exec'
9
9
  TAP_DEBUG = 'MDE_DEBUG'
10
- VERSION = '2.7.5'
10
+ VERSION = '2.8.0'
11
11
  end
data/lib/markdown_exec.rb CHANGED
@@ -256,7 +256,7 @@ module MarkdownExec
256
256
  block_name_nick_match)
257
257
  return unless line.match(fenced_start_and_end_regex)
258
258
 
259
- bm = NamedCaptureExtractor::extract_named_groups(line, block_name_match)
259
+ bm = NamedCaptureExtractor.extract_named_groups(line, block_name_match)
260
260
  return if bm.nil?
261
261
 
262
262
  name = bm[:title]
@@ -282,7 +282,6 @@ module MarkdownExec
282
282
  include StringUtil
283
283
 
284
284
  def initialize(options = {})
285
- # ww0 'options', options, caller.deref
286
285
  @option_parser = nil
287
286
 
288
287
  @options = HashDelegator.new(options)
@@ -405,7 +404,7 @@ module MarkdownExec
405
404
  ## Executes the block specified in the options
406
405
  #
407
406
  def execute_block_with_error_handling
408
- @options.register_console_attributes(@options)
407
+ @options.register_console_attributes(@options)
409
408
  finalize_cli_argument_processing
410
409
  execute_initial_commands_and_main_loop(@options, @options.run_state)
411
410
  rescue FileMissingError
@@ -430,11 +429,9 @@ module MarkdownExec
430
429
  def execute_simple_commands(options, stage: nil)
431
430
  # !!p stage
432
431
  simple_commands(options).each do |key, (cstage, proc)|
433
- if @options[key].is_a?(TrueClass) || @options[key].present?
434
- if stage && stage == cstage
435
- proc.call
436
- return true
437
- end
432
+ if (@options[key].is_a?(TrueClass) || @options[key].present?) && (stage && stage == cstage)
433
+ proc.call
434
+ return true
438
435
  end
439
436
  end
440
437
  false
data/lib/mdoc.rb CHANGED
@@ -142,6 +142,7 @@ module MarkdownExec
142
142
  fcb.body # entire body is returned to requesing block
143
143
  elsif [BlockType::LINK,
144
144
  BlockType::LOAD,
145
+ BlockType::UX,
145
146
  BlockType::VARS].include? fcb.type
146
147
  nil
147
148
  elsif fcb[:chrome] # for Link blocks like History
data/lib/menu.src.yml CHANGED
@@ -206,6 +206,12 @@
206
206
  :default: "(document_shell)"
207
207
  :procname: val_as_str
208
208
 
209
+ - :opt_name: document_load_ux_block_name
210
+ :env_var: MDE_DOCUMENT_LOAD_UX_BLOCK_NAME
211
+ :description: Name of UX block to load with the document
212
+ :default: "\\[.*document_ux.*\\]"
213
+ :procname: val_as_str
214
+
209
215
  - :opt_name: document_load_vars_block_name
210
216
  :env_var: MDE_DOCUMENT_LOAD_VARS_BLOCK_NAME
211
217
  :description: Name of Vars block to load with the document
@@ -1026,6 +1032,18 @@
1026
1032
  :default: ">"
1027
1033
  :procname: val_as_str
1028
1034
 
1035
+ - :opt_name: menu_ux_color
1036
+ :env_var: MDE_MENU_UX_COLOR
1037
+ :description: Color of menu ux
1038
+ :default: fg_rgbh_df_c0_df
1039
+ :procname: val_as_str
1040
+
1041
+ - :opt_name: menu_ux_row_format
1042
+ :env_var: MDE_MENU_UX_ROW_FORMAT
1043
+ :description: Format for UX row
1044
+ :default: '%{name}=${%{name}}'
1045
+ :procname: val_as_str
1046
+
1029
1047
  - :opt_name: menu_vars_color
1030
1048
  :env_var: MDE_MENU_VARS_COLOR
1031
1049
  :description: Color of menu vars
@@ -1315,6 +1333,12 @@
1315
1333
  :description: Prompt to select a saved file
1316
1334
  :procname: val_as_str
1317
1335
 
1336
+ - :opt_name: prompt_ux_enter_a_value
1337
+ :env_var: MDE_PROMPT_UX_ENTER_A_VALUE
1338
+ :description: Prompt to enter a value for UX blocks
1339
+ :default: "\nEnter a value:"
1340
+ :procname: val_as_str
1341
+
1318
1342
  - :opt_name: prompt_show_expr_format
1319
1343
  :env_var: MDE_PROMPT_SHOW_EXPR_FORMAT
1320
1344
  :description: prompt_show_expr_format
@@ -1630,6 +1654,12 @@
1630
1654
  :default: false
1631
1655
  :procname: val_as_bool
1632
1656
 
1657
+ - :opt_name: ux_auto_load_force_default
1658
+ :env_var: MDE_UX_AUTO_LOAD_FORCE_DEFAULT
1659
+ :arg_name: BOOL
1660
+ :default: true
1661
+ :procname: val_as_bool
1662
+
1633
1663
  - :opt_name: variable_expression_regexp
1634
1664
  :env_var: MDE_VARIABLE_EXPRESSION_REGEXP
1635
1665
  :description: variable_expression_regexp
data/lib/menu.yml CHANGED
@@ -171,6 +171,11 @@
171
171
  :description: Name of shell block to load with the document
172
172
  :default: "(document_shell)"
173
173
  :procname: val_as_str
174
+ - :opt_name: document_load_ux_block_name
175
+ :env_var: MDE_DOCUMENT_LOAD_UX_BLOCK_NAME
176
+ :description: Name of UX block to load with the document
177
+ :default: "\\[.*document_ux.*\\]"
178
+ :procname: val_as_str
174
179
  - :opt_name: document_load_vars_block_name
175
180
  :env_var: MDE_DOCUMENT_LOAD_VARS_BLOCK_NAME
176
181
  :description: Name of Vars block to load with the document
@@ -864,6 +869,16 @@
864
869
  :description: Symbol before each task
865
870
  :default: ">"
866
871
  :procname: val_as_str
872
+ - :opt_name: menu_ux_color
873
+ :env_var: MDE_MENU_UX_COLOR
874
+ :description: Color of menu ux
875
+ :default: fg_rgbh_df_c0_df
876
+ :procname: val_as_str
877
+ - :opt_name: menu_ux_row_format
878
+ :env_var: MDE_MENU_UX_ROW_FORMAT
879
+ :description: Format for UX row
880
+ :default: "%{name}=${%{name}}"
881
+ :procname: val_as_str
867
882
  - :opt_name: menu_vars_color
868
883
  :env_var: MDE_MENU_VARS_COLOR
869
884
  :description: Color of menu vars
@@ -1123,6 +1138,13 @@
1123
1138
  Choose a file:
1124
1139
  :description: Prompt to select a saved file
1125
1140
  :procname: val_as_str
1141
+ - :opt_name: prompt_ux_enter_a_value
1142
+ :env_var: MDE_PROMPT_UX_ENTER_A_VALUE
1143
+ :description: Prompt to enter a value for UX blocks
1144
+ :default: |2-
1145
+
1146
+ Enter a value:
1147
+ :procname: val_as_str
1126
1148
  - :opt_name: prompt_show_expr_format
1127
1149
  :env_var: MDE_PROMPT_SHOW_EXPR_FORMAT
1128
1150
  :description: prompt_show_expr_format
@@ -1391,6 +1413,11 @@
1391
1413
  :arg_name: BOOL
1392
1414
  :default: false
1393
1415
  :procname: val_as_bool
1416
+ - :opt_name: ux_auto_load_force_default
1417
+ :env_var: MDE_UX_AUTO_LOAD_FORCE_DEFAULT
1418
+ :arg_name: BOOL
1419
+ :default: true
1420
+ :procname: val_as_bool
1394
1421
  - :opt_name: variable_expression_regexp
1395
1422
  :env_var: MDE_VARIABLE_EXPRESSION_REGEXP
1396
1423
  :description: variable_expression_regexp
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: markdown_exec
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.7.5
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fareed Stevenson
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-02-02 00:00:00.000000000 Z
11
+ date: 2025-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: clipboard
@@ -124,6 +124,7 @@ files:
124
124
  - bats/options.bats
125
125
  - bats/plain.bats
126
126
  - bats/publish.bats
127
+ - bats/table-column-truncate.bats
127
128
  - bats/table.bats
128
129
  - bats/test_helper.bash
129
130
  - bats/variable-expansion.bats
@@ -159,6 +160,7 @@ files:
159
160
  - docs/dev/screen-width.md
160
161
  - docs/dev/specs-import.md
161
162
  - docs/dev/specs.md
163
+ - docs/dev/table-column-truncate.md
162
164
  - docs/dev/table-indent.md
163
165
  - docs/dev/table-invalid.md
164
166
  - docs/dev/text-decoration.md