asciidoctor-dita-map 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 785383f1b9ac91dfff356c6ccfce6568a16af5c805715d24d294f8539d019d89
4
+ data.tar.gz: 0be9258a03b8cff762cd30aae0657b6d105636b1a92334ed456ae68f57eabf1b
5
+ SHA512:
6
+ metadata.gz: d9dcb68378da10bd98c006524283da2741f32028988dcd2236ecaef3e5933e44a333f8502d8656e94c29dffff1373b542ed2efc75b48e6130d35cd5722395ac9
7
+ data.tar.gz: c550422cc1932cc96c952c26a978d48d7788a8b5c9700ad3c51c1f544f29407d780fc5c13a25b06b11e78ae4b7b0751ab26caecf01020c449d94397b10b4d6db
data/AUTHORS ADDED
@@ -0,0 +1 @@
1
+ Jaromir Hradilek <jhradilek@gmail.com>
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (C) 2026 Jaromir Hradilek
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a
6
+ copy of this software and associated documentation files (the "Software"),
7
+ to deal in the Software without restriction, including without limitation
8
+ the rights to use, copy, modify, merge, publish, distribute, sublicense,
9
+ and/or sell copies of the Software, and to permit persons to whom the
10
+ Software is furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,9 @@
1
+ # dita-map
2
+
3
+ **dita-map** is a command line utility that converts a single AsciiDoc file to a DITA map.
4
+
5
+ ## Copyright
6
+
7
+ Copyright © 2026 Jaromir Hradilek
8
+
9
+ This program is free software, released under the terms of the MIT license. It is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
data/bin/dita-map ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # Copyright (C) 2026 Jaromir Hradilek
4
+
5
+ # MIT License
6
+ #
7
+ # Permission is hereby granted, free of charge, to any person obtaining
8
+ # a copy of this software and associated documentation files (the "Soft-
9
+ # ware"), to deal in the Software without restriction, including without
10
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
11
+ # sublicense, and/or sell copies of the Software, and to permit persons to
12
+ # whom the Software is furnished to do so, subject to the following condi-
13
+ # tions:
14
+ #
15
+ # The above copyright notice and this permission notice shall be included
16
+ # in all copies or substantial portions of the Software.
17
+ #
18
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABI-
20
+ # LITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
22
+ # OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
23
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
24
+ # OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ require (cli = File.absolute_path '../lib/dita-map/cli.rb', __dir__) ? cli : 'dita-map/cli'
27
+
28
+ begin
29
+ name = File.basename($0)
30
+ converter = AsciidoctorDitaMap::Cli.new name, ARGV
31
+ converter.run
32
+ rescue OptionParser::InvalidArgument, OptionParser::InvalidOption, OptionParser::MissingArgument => error
33
+ abort "#{name}: #{error.message}"
34
+ rescue Interrupt
35
+ exit 130
36
+ end
@@ -0,0 +1,43 @@
1
+ # Copyright (C) 2026 Jaromir Hradilek
2
+
3
+ # MIT License
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the "Soft-
7
+ # ware"), to deal in the Software without restriction, including without
8
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
9
+ # sublicense, and/or sell copies of the Software, and to permit persons to
10
+ # whom the Software is furnished to do so, subject to the following condi-
11
+ # tions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included
14
+ # in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABI-
18
+ # LITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
20
+ # OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'asciidoctor'
25
+
26
+ class CatalogIncludeDirectives < Asciidoctor::Extensions::IncludeProcessor
27
+ def handles? target
28
+ target.end_with? '.adoc', '.asciidoc', '.asc', '.ad'
29
+ end
30
+
31
+ def process doc, reader, target, attributes
32
+ offset = attributes['leveloffset'].to_i
33
+
34
+ doc.catalog[:include_files] = [] unless doc.catalog[:include_files]
35
+ doc.catalog[:include_files].append({
36
+ :target => target,
37
+ :offset => offset
38
+ })
39
+
40
+ reader
41
+ end
42
+ end
43
+
@@ -0,0 +1,258 @@
1
+ # Copyright (C) 2026 Jaromir Hradilek
2
+
3
+ # MIT License
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the "Soft-
7
+ # ware"), to deal in the Software without restriction, including without
8
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
9
+ # sublicense, and/or sell copies of the Software, and to permit persons to
10
+ # whom the Software is furnished to do so, subject to the following condi-
11
+ # tions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included
14
+ # in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABI-
18
+ # LITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
20
+ # OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ require 'optparse'
25
+ require 'pathname'
26
+ require 'asciidoctor'
27
+ require 'rexml/document'
28
+ require_relative 'catalog'
29
+ require_relative 'version'
30
+
31
+ module AsciidoctorDitaMap
32
+ class Cli
33
+ def initialize name, argv
34
+ @attr = []
35
+ @opts = {
36
+ :id => true,
37
+ :navtitle => true,
38
+ :output => false,
39
+ :title => true,
40
+ :type => true
41
+ }
42
+ @prep = []
43
+ @name = name
44
+ @args = self.parse_args argv
45
+ end
46
+
47
+ def parse_args argv
48
+ parser = OptionParser.new do |opt|
49
+ opt.banner = "Usage: #{@name} [OPTION...] [FILE...]\n"
50
+ opt.banner += " #{@name} -h|-v\n\n"
51
+
52
+ opt.on('-o', '--out-file FILE', 'specify the output file; by default, the output file name is based on the input file') do |output|
53
+ @opts[:output] = (output.strip == '-') ? $stdout : output
54
+ end
55
+
56
+ opt.on('-a', '--attribute ATTRIBUTE', 'set a document attribute in the form of name, name!, or name=value pair; can be supplied multiple times') do |value|
57
+ @attr.append value
58
+ end
59
+
60
+ opt.separator ''
61
+
62
+ opt.on('-p', '--prepend-file FILE', 'prepend a file to all input files; can be supplied multiple times') do |file|
63
+ raise OptionParser::InvalidArgument, "not a file: #{file}" unless File.exist? file and File.file? file
64
+ raise OptionParser::InvalidArgument, "file not readable: #{file}" unless File.readable? file
65
+
66
+ @prep.append file
67
+ end
68
+
69
+ opt.separator ''
70
+
71
+ opt.on('-I', '--no-id', 'do not generate the map id attribute') do
72
+ @opts[:id] = false
73
+ end
74
+
75
+ opt.on('-M', '--no-maptitle', 'do not generate the map title') do
76
+ @opts[:title] = false
77
+ end
78
+
79
+ opt.on('-N', '--no-navtitle', 'do not generate the navtitle attribute') do
80
+ @opts[:navtitle] = false
81
+ end
82
+
83
+ opt.on('-T', '--no-type', 'do not generate the type attribute') do
84
+ @opts[:type] = false
85
+ end
86
+
87
+ opt.separator ''
88
+
89
+ opt.on('-h', '--help', 'display this help and exit') do
90
+ puts opt
91
+ exit
92
+ end
93
+
94
+ opt.on('-v', '--version', 'display version information and exit') do
95
+ puts "#{@name} #{VERSION}"
96
+ exit
97
+ end
98
+ end
99
+
100
+ args = parser.parse argv
101
+
102
+ if args.length == 0 or args[0].strip == '-'
103
+ return [$stdin]
104
+ end
105
+
106
+ args.each do |file|
107
+ raise OptionParser::InvalidArgument, "not a file: #{file}" unless File.exist? file and File.file? file
108
+ raise OptionParser::InvalidArgument, "file not readable: #{file}" unless File.readable? file
109
+ end
110
+
111
+ return args
112
+ end
113
+
114
+ def parse_topic input
115
+ doc = Asciidoctor.load input, safe: :secure, attributes: @attr
116
+ att = doc.attributes
117
+
118
+ document_title = doc.title ? doc.title.gsub(/<[^>]*>/, '') : nil
119
+ document_type = att['_mod-docs-content-type'] ? att['_mod-docs-content-type'].downcase : nil
120
+ document_type = att['_content-type'] ? att['_content-type'].downcase : nil unless document_type
121
+ document_type = att['_module-type'] ? att['_module-type'].downcase : nil unless document_type
122
+
123
+ if document_type
124
+ document_type.sub!(/^assembly$/, 'concept')
125
+ document_type.sub!(/^procedure$/, 'task')
126
+ end
127
+
128
+ unless ['concept', 'reference', 'task', 'map'].include? document_type
129
+ document_type = nil
130
+ end
131
+
132
+ return document_title, document_type
133
+ end
134
+
135
+
136
+ def parse_map input, base_dir
137
+ Asciidoctor::Extensions.register do
138
+ include_processor CatalogIncludeDirectives
139
+ end
140
+
141
+ doc = Asciidoctor.load input, safe: :safe, catalog_assets: true, attributes: @attr, base_dir: base_dir
142
+
143
+ include_files = doc.catalog[:include_files] ? doc.catalog[:include_files] : []
144
+ document_title = doc.title ? doc.title.gsub(/<[^>]*>/, '') : nil
145
+ document_id = doc.id ? doc.id.gsub(/["']/, '') : nil
146
+
147
+ return include_files, document_title, document_id
148
+ end
149
+
150
+ def convert_map input, base_dir, prepended = ''
151
+ result = ''
152
+
153
+ include_files, map_title, document_id = parse_map prepended + input, base_dir
154
+
155
+ xml = REXML::Document.new
156
+ xml.context[:attribute_quote] = :quote
157
+ xml << REXML::XMLDecl.new('1.0', 'utf-8')
158
+ xml << REXML::DocType.new('map', 'PUBLIC "-//OASIS//DTD DITA Map//EN" "map.dtd"')
159
+
160
+ if document_id and @opts[:id]
161
+ xml_root = xml.add_element('map', { 'id' => document_id })
162
+ else
163
+ xml_root = xml.add_element('map')
164
+ end
165
+
166
+ if map_title and @opts[:title]
167
+ xml_title = xml_root.add_element('title')
168
+ xml_title.text = map_title
169
+ end
170
+
171
+ stack = [{ :offset => 0, :element => xml_root }]
172
+
173
+ include_files.each do |file|
174
+ target = file[:target]
175
+ offset = file[:offset]
176
+ last_offset = stack.last[:offset]
177
+
178
+ if offset == 0
179
+ warn "#{@name}: warning: Invalid leveloffset - expected 1, got 0: #{target}"
180
+ offset = 1
181
+ elsif offset > last_offset and offset - last_offset > 1
182
+ expected_offset = last_offset + 1
183
+ warn "#{@name}: warning: Invalid leveloffset - expected #{expected_offset}, got #{offset}: #{target}"
184
+ offset = expected_offset
185
+ end
186
+
187
+ while stack.last[:offset] >= offset
188
+ stack.pop
189
+ end
190
+
191
+ xml_parent = stack.last[:element]
192
+
193
+ if @opts[:navtitle] or @opts[:type]
194
+ begin
195
+ include_title, include_type = parse_topic prepended + File.read(base_dir + target)
196
+ rescue
197
+ warn "#{@name}: warning: Unable to read included file: #{base_dir + target}"
198
+ include_title, include_type = nil, nil
199
+ end
200
+ end
201
+
202
+ if include_type == 'map'
203
+ file_name = target.sub(/\.adoc$/, '.ditamap')
204
+ attributes = { 'href' => file_name, 'format' => 'ditamap' }
205
+ attributes['type'] = include_type if @opts[:type]
206
+
207
+ xml_element = xml_parent.add_element('mapref', attributes)
208
+ else
209
+ file_name = target.sub(/\.adoc$/, '.dita')
210
+ attributes = { 'href' => file_name }
211
+ attributes['navtitle'] = include_title if include_title and @opts[:navtitle]
212
+ attributes['type'] = include_type if include_type and @opts[:type]
213
+
214
+ xml_element = xml_parent.add_element('topicref', attributes)
215
+ end
216
+
217
+ stack.push ({ :offset => offset, :element => xml_element })
218
+ end
219
+
220
+ formatter = REXML::Formatters::Pretty.new(2, true)
221
+ formatter.compact = true
222
+ formatter.write(xml, result)
223
+
224
+ result << "\n"
225
+
226
+ return result
227
+ end
228
+
229
+ def run
230
+ prepended = ''
231
+
232
+ @prep.each do |file|
233
+ prepended << File.read(file)
234
+ prepended << "\n"
235
+ end
236
+
237
+ @args.each do |file|
238
+ if file == $stdin
239
+ base_dir = Pathname.new(Dir.pwd).expand_path
240
+ input = $stdin.read
241
+ output = @opts[:output] ? @opts[:output] : $stdout
242
+ else
243
+ base_dir = Pathname.new(file).dirname.expand_path
244
+ input = File.read(file)
245
+ output = @opts[:output] ? @opts[:output] : Pathname.new(file).sub_ext('.ditamap').to_s
246
+ end
247
+
248
+ result = convert_map input, base_dir, prepended
249
+
250
+ if output == $stdout
251
+ $stdout.write result
252
+ else
253
+ File.write output, result
254
+ end
255
+ end
256
+ end
257
+ end
258
+ end
@@ -0,0 +1,28 @@
1
+ # Copyright (C) 2026 Jaromir Hradilek
2
+
3
+ # MIT License
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the "Soft-
7
+ # ware"), to deal in the Software without restriction, including without
8
+ # limitation the rights to use, copy, modify, merge, publish, distribute,
9
+ # sublicense, and/or sell copies of the Software, and to permit persons to
10
+ # whom the Software is furnished to do so, subject to the following condi-
11
+ # tions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included
14
+ # in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
17
+ # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABI-
18
+ # LITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ # SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
20
+ # OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
+ # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ # OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ # frozen_string_literal: true
25
+
26
+ module AsciidoctorDitaMap
27
+ VERSION = '0.1.0'
28
+ end
metadata ADDED
@@ -0,0 +1,145 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: asciidoctor-dita-map
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jaromir Hradilek
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: asciidoctor
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '2.0'
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: 2.0.26
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '2.0'
29
+ - - ">="
30
+ - !ruby/object:Gem::Version
31
+ version: 2.0.26
32
+ - !ruby/object:Gem::Dependency
33
+ name: rexml
34
+ requirement: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - "~>"
37
+ - !ruby/object:Gem::Version
38
+ version: '3.4'
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 3.4.4
42
+ type: :runtime
43
+ prerelease: false
44
+ version_requirements: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '3.4'
49
+ - - ">="
50
+ - !ruby/object:Gem::Version
51
+ version: 3.4.4
52
+ - !ruby/object:Gem::Dependency
53
+ name: rake
54
+ requirement: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - "~>"
57
+ - !ruby/object:Gem::Version
58
+ version: '13.3'
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 13.3.1
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.3'
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: 13.3.1
72
+ - !ruby/object:Gem::Dependency
73
+ name: minitest
74
+ requirement: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - "~>"
77
+ - !ruby/object:Gem::Version
78
+ version: '6.0'
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 6.0.2
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: '6.0'
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: 6.0.2
92
+ - !ruby/object:Gem::Dependency
93
+ name: minitest-mock
94
+ requirement: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: '5.27'
99
+ type: :development
100
+ prerelease: false
101
+ version_requirements: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - "~>"
104
+ - !ruby/object:Gem::Version
105
+ version: '5.27'
106
+ description: A command line utility that converts a single AsciiDoc file to a DITA
107
+ map.
108
+ email: jhradilek@gmail.com
109
+ executables:
110
+ - dita-map
111
+ extensions: []
112
+ extra_rdoc_files: []
113
+ files:
114
+ - AUTHORS
115
+ - LICENSE
116
+ - README.md
117
+ - bin/dita-map
118
+ - lib/dita-map/catalog.rb
119
+ - lib/dita-map/cli.rb
120
+ - lib/dita-map/version.rb
121
+ homepage: https://github.com/jhradilek/asciidoctor-dita-map
122
+ licenses:
123
+ - MIT
124
+ metadata:
125
+ homepage_uri: https://github.com/jhradilek/asciidoctor-dita-map
126
+ bug_tracker_uri: https://github.com/jhradilek/asciidoctor-dita-map/issues
127
+ documentation_uri: https://github.com/jhradilek/asciidoctor-dita-map/blob/main/README.md
128
+ rdoc_options: []
129
+ require_paths:
130
+ - lib
131
+ required_ruby_version: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '3.2'
136
+ required_rubygems_version: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ requirements: []
142
+ rubygems_version: 3.6.9
143
+ specification_version: 4
144
+ summary: Convert an AsciiDoc file to a DITA map
145
+ test_files: []