alt_printf 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 +7 -0
- data/Gemfile +2 -0
- data/README.md +14 -0
- data/Rakefile +53 -0
- data/alt_printf.gemspec +24 -0
- data/ext/alt_printf/alt_printf.c +222 -0
- data/ext/alt_printf/altprintf.c +244 -0
- data/ext/alt_printf/altprintf.h +42 -0
- data/ext/alt_printf/extconf.rb +5 -0
- data/ext/alt_printf/extconf_dev.rb +9 -0
- data/ext/alt_printf/list.c +81 -0
- data/ext/alt_printf/list.h +24 -0
- data/ext/alt_printf/log.h +11 -0
- data/ext/alt_printf/strbuf.c +142 -0
- data/ext/alt_printf/strbuf.h +28 -0
- data/ext/alt_printf/syntax.h +30 -0
- data/lib/alt_printf/alt_printf.so +0 -0
- data/lib/alt_printf/version.rb +3 -0
- data/lib/alt_printf.rb +4 -0
- metadata +89 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ca3bfbd5642eb9ee9d75ed799b3cb18708d44c2d1f57eabcf8cf9795cc54e82c
|
4
|
+
data.tar.gz: e7d66a6c4770522c112d677de2c3d88df98ddb2a0089ddd784b10063493a21cb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fe5d82f1920848129f2bf490bb49754efd1de8b46dd7c468cc700b6a5e9d76a5b130706bcbfff1d87faa000f8a933247886bff4c2c2fdba17d5f398f7b822dac
|
7
|
+
data.tar.gz: 5fe9d231104bcfc0d5fda40924c3f55630c3981427f09ab63985432123f314250b858f2bb7dd946ae58161702abd832e2e81f4a4f06a8b58ab7f56b86a80fcfd
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# AltPrintf
|
2
|
+
|
3
|
+
AltPrintf is a gem that wraps `altprintf`.
|
4
|
+
|
5
|
+
It exposes the following module functions
|
6
|
+
|
7
|
+
+ `#fmt(format_string, *args, **kwargs)`
|
8
|
+
+ `#fmtm(passes, format_string, *args, **kwargs)`
|
9
|
+
|
10
|
+
In addition to the syntax of vanilla altprintf, the following additional
|
11
|
+
argument is accepted:
|
12
|
+
|
13
|
+
+ `<val>` - access the value of the given hash with key `:"#{val}"` rather than
|
14
|
+
try to read the next argument.
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'rake/extensiontask'
|
2
|
+
require 'bundler/gem_tasks'
|
3
|
+
require 'securerandom'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
SRC_DIR = File.join(__dir__, '../src')
|
7
|
+
|
8
|
+
Rake::ExtensionTask.new('alt_printf') do |ext|
|
9
|
+
ext.lib_dir = 'lib/alt_printf'
|
10
|
+
ext.config_script = 'extconf_dev.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
task :build_gem do
|
14
|
+
tmp_dir = "/tmp/alt_printf_#{SecureRandom.hex(8)}"
|
15
|
+
puts "cloning to #{tmp_dir}"
|
16
|
+
FileUtils.mkdir(tmp_dir)
|
17
|
+
|
18
|
+
Dir[
|
19
|
+
'Gemfile',
|
20
|
+
'README.md',
|
21
|
+
'Rakefile',
|
22
|
+
'alt_printf.gemspec',
|
23
|
+
'{ext,lib}/**/*'
|
24
|
+
].each do |f|
|
25
|
+
next if File.directory?(f)
|
26
|
+
|
27
|
+
dir = File.join(tmp_dir, File.dirname(f))
|
28
|
+
FileUtils.mkdir_p(dir)
|
29
|
+
FileUtils.cp(f, File.join(dir, File.basename(f)))
|
30
|
+
end
|
31
|
+
|
32
|
+
FileUtils.cp_r(
|
33
|
+
%w[altprintf list log strbuf syntax].map do |w|
|
34
|
+
%w[h c].map { |e| File.join(SRC_DIR, "#{w}.#{e}") }
|
35
|
+
.select { |f| File.exist?(f) }
|
36
|
+
end.flatten,
|
37
|
+
File.join(tmp_dir, 'ext/alt_printf/')
|
38
|
+
)
|
39
|
+
|
40
|
+
FileUtils.cd(tmp_dir)
|
41
|
+
|
42
|
+
sh "rake build"
|
43
|
+
|
44
|
+
FileUtils.cp(Dir['pkg/*'], File.join(__dir__, 'pkg'))
|
45
|
+
FileUtils.cd(__dir__)
|
46
|
+
FileUtils.rm_rf(tmp_dir)
|
47
|
+
end
|
48
|
+
|
49
|
+
task publish: :build_gem do
|
50
|
+
load File.join(__dir__, 'alt_printf.gemspec')
|
51
|
+
p AltPrintf
|
52
|
+
sh "gem push pkg/#{AltPrintf::SPEC.name}-#{AltPrintf::SPEC.version}.gem"
|
53
|
+
end
|
data/alt_printf.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'date'
|
2
|
+
require_relative 'lib/alt_printf/version'
|
3
|
+
|
4
|
+
AltPrintf::SPEC = Gem::Specification.new do |s|
|
5
|
+
s.name = 'alt_printf'
|
6
|
+
s.version = AltPrintf::VERSION.join('.')
|
7
|
+
s.date = Date.today.strftime('%Y-%m-%d')
|
8
|
+
s.summary = 'A powerful printf-like template language'
|
9
|
+
s.authors = ['Stone Tickle']
|
10
|
+
s.email = 'lattis@mochiro.moe'
|
11
|
+
s.homepage = 'https://github.com/alt_printf/gem'
|
12
|
+
s.license = 'MIT'
|
13
|
+
|
14
|
+
s.files = Dir['{**/*}']
|
15
|
+
|
16
|
+
s.platform = Gem::Platform::RUBY
|
17
|
+
s.extensions = Dir["ext/**/extconf.rb"]
|
18
|
+
s.require_paths = ['lib']
|
19
|
+
|
20
|
+
s.required_ruby_version = '>= 2.6.3'
|
21
|
+
|
22
|
+
s.add_development_dependency 'rake-compiler', '~> 1.0'
|
23
|
+
s.add_development_dependency 'rake', '~> 12.3'
|
24
|
+
end unless AltPrintf.const_defined?(:'SPEC')
|
@@ -0,0 +1,222 @@
|
|
1
|
+
#include <stdio.h>
|
2
|
+
#include <locale.h>
|
3
|
+
#include <wchar.h>
|
4
|
+
#include <ruby.h>
|
5
|
+
#include <ruby/encoding.h>
|
6
|
+
#include "extconf.h"
|
7
|
+
#include "list.h"
|
8
|
+
#include "altprintf.h"
|
9
|
+
#include "log.h"
|
10
|
+
|
11
|
+
#define CHECKARG \
|
12
|
+
if (!use_hash) { \
|
13
|
+
if (*argi >= argc) goto no_more_args; \
|
14
|
+
entry = rb_ary_entry(*argv, *argi); \
|
15
|
+
}
|
16
|
+
|
17
|
+
#define MODNAME "AltPrintf"
|
18
|
+
|
19
|
+
rb_encoding *enc;
|
20
|
+
|
21
|
+
wchar_t *rbstowcs(VALUE str) {
|
22
|
+
const char *cstr;
|
23
|
+
wchar_t *wstr;
|
24
|
+
size_t len;
|
25
|
+
|
26
|
+
cstr = StringValueCStr(str);
|
27
|
+
|
28
|
+
len = mbsrtowcs(NULL, &cstr, 0, NULL);
|
29
|
+
wstr = calloc(len + 1, sizeof(wchar_t));
|
30
|
+
len = mbsrtowcs(wstr, &cstr, len, NULL);
|
31
|
+
|
32
|
+
LOG("rbs to wcs, len: %d, cstr: '%s'\n", len, cstr);
|
33
|
+
LOG("wide string: '%ls'\n", wstr);
|
34
|
+
|
35
|
+
return wstr;
|
36
|
+
}
|
37
|
+
|
38
|
+
VALUE wcstorbs(const wchar_t *wstr) {
|
39
|
+
size_t len;
|
40
|
+
char *cstr;
|
41
|
+
VALUE str;
|
42
|
+
|
43
|
+
len = wcsrtombs(NULL, &wstr, 0, NULL);
|
44
|
+
cstr = calloc(len, sizeof(wchar_t));
|
45
|
+
wcsrtombs(cstr, &wstr, len, NULL);
|
46
|
+
|
47
|
+
LOG("wcs to rbs, len: %d, cstr: '%s'\n", len, cstr);
|
48
|
+
|
49
|
+
str = rb_external_str_new_with_enc(cstr, len, enc);
|
50
|
+
free(cstr);
|
51
|
+
|
52
|
+
return str;
|
53
|
+
}
|
54
|
+
|
55
|
+
struct list_elem *rb_altprintf_make_list(const wchar_t *fmt, VALUE *argv, long *argi, VALUE *hash) {
|
56
|
+
struct list_elem *le_cur;
|
57
|
+
struct list_elem *le_start;
|
58
|
+
struct list_elem *le_prev;
|
59
|
+
/* create a dummy element as the head */
|
60
|
+
le_start = le_prev = list_elem_create();
|
61
|
+
|
62
|
+
VALUE entry, symbol;
|
63
|
+
|
64
|
+
long int *tmp_int;
|
65
|
+
wint_t *tmp_char;
|
66
|
+
double *tmp_double;
|
67
|
+
const wchar_t *tmp_str;
|
68
|
+
|
69
|
+
char *cstr;
|
70
|
+
size_t len;
|
71
|
+
|
72
|
+
int mode = 0;
|
73
|
+
long argc = rb_array_len(*argv);
|
74
|
+
int use_hash = 0;
|
75
|
+
|
76
|
+
const wchar_t *end = &fmt[wcslen(fmt)];
|
77
|
+
|
78
|
+
for (;fmt<end;fmt++) {
|
79
|
+
LOG("checking char '%lc', lvl: '%d'\n", (wint_t)(*fmt), mode);
|
80
|
+
if (mode == 0) {
|
81
|
+
switch(*fmt) {
|
82
|
+
case FS_START: mode = 1;
|
83
|
+
break;
|
84
|
+
}
|
85
|
+
} else {
|
86
|
+
switch (*fmt) {
|
87
|
+
case FS_A_CHARARG:
|
88
|
+
fmt++;
|
89
|
+
break;
|
90
|
+
case FS_A_RBHASHSTART:
|
91
|
+
tmp_str = fmt + 1;
|
92
|
+
|
93
|
+
use_hash = -1;
|
94
|
+
while (fmt < end && *fmt != FS_A_RBHASHEND) {
|
95
|
+
fmt++;
|
96
|
+
use_hash++;
|
97
|
+
}
|
98
|
+
|
99
|
+
len = wcsnrtombs(NULL, &fmt, use_hash, 0, NULL);
|
100
|
+
cstr = calloc(len + 1, sizeof(char));
|
101
|
+
wcsnrtombs(cstr, &tmp_str, use_hash, len, NULL);
|
102
|
+
LOG("cstr: '%s'\n", cstr);
|
103
|
+
|
104
|
+
symbol = rb_check_symbol_cstr(cstr, len, enc);
|
105
|
+
entry = rb_hash_lookup2(*hash, symbol, Qnil);
|
106
|
+
free(cstr);
|
107
|
+
use_hash = 1;
|
108
|
+
|
109
|
+
break;
|
110
|
+
case FS_A_STRINGSTART:
|
111
|
+
while (fmt < end && *fmt != FS_A_STRINGEND) fmt++;
|
112
|
+
break;
|
113
|
+
case FS_T_STRING:
|
114
|
+
CHECKARG;
|
115
|
+
|
116
|
+
tmp_str = rbstowcs(entry);
|
117
|
+
le_cur = list_elem_ini(tmp_str, String);
|
118
|
+
goto match;
|
119
|
+
case FS_T_MUL:
|
120
|
+
case FS_T_TERN:
|
121
|
+
case FS_T_ALIGN:
|
122
|
+
case FS_T_INT:
|
123
|
+
CHECKARG;
|
124
|
+
|
125
|
+
tmp_int = malloc(sizeof(long int));
|
126
|
+
*tmp_int = FIX2LONG(entry);
|
127
|
+
LOG("got int %ld\n", *tmp_int);
|
128
|
+
le_cur = list_elem_ini(tmp_int, Int);
|
129
|
+
goto match;
|
130
|
+
case FS_T_CHAR:
|
131
|
+
CHECKARG;
|
132
|
+
|
133
|
+
tmp_char = malloc(sizeof(wint_t));
|
134
|
+
tmp_str = rbstowcs(entry);
|
135
|
+
*tmp_char = btowc(tmp_str[0]);
|
136
|
+
le_cur = list_elem_ini(tmp_char, Char);
|
137
|
+
goto match;
|
138
|
+
case FS_T_DOUBLE:
|
139
|
+
CHECKARG;
|
140
|
+
|
141
|
+
tmp_double = malloc(sizeof(double));
|
142
|
+
*tmp_double = RFLOAT_VALUE(entry);
|
143
|
+
le_cur = list_elem_ini(tmp_double, Double);
|
144
|
+
goto match;
|
145
|
+
match: le_prev->next = le_cur;
|
146
|
+
le_prev = le_cur;
|
147
|
+
mode = 0;
|
148
|
+
(*argi)++;
|
149
|
+
break;
|
150
|
+
case FS_START:
|
151
|
+
mode = 0;
|
152
|
+
break;
|
153
|
+
}
|
154
|
+
}
|
155
|
+
}
|
156
|
+
|
157
|
+
no_more_args:
|
158
|
+
if (le_start->next == NULL) return le_start;
|
159
|
+
|
160
|
+
/* set cur to the 2nd element and destroy the first one */
|
161
|
+
le_cur = le_start->next;
|
162
|
+
le_start->next = NULL;
|
163
|
+
list_elem_destroy(le_start);
|
164
|
+
|
165
|
+
return le_cur;
|
166
|
+
}
|
167
|
+
|
168
|
+
VALUE rb_alt_printf(long passes, size_t argc, VALUE *argv, VALUE self) {
|
169
|
+
VALUE fmt, args, hash, final;
|
170
|
+
wchar_t *wfmt;
|
171
|
+
wchar_t *formatted;
|
172
|
+
struct list_elem *ap;
|
173
|
+
|
174
|
+
rb_scan_args(argc, argv, "1*:", &fmt, &args, &hash);
|
175
|
+
|
176
|
+
if (passes == 0) return fmt;
|
177
|
+
|
178
|
+
wfmt = rbstowcs(fmt);
|
179
|
+
|
180
|
+
long argi = 0;
|
181
|
+
while (passes > 0) {
|
182
|
+
ap = rb_altprintf_make_list(wfmt, &args, &argi, &hash);
|
183
|
+
|
184
|
+
formatted = altsprintf(wfmt, ap);
|
185
|
+
LOG("formatted result: '%ls'\n", formatted);
|
186
|
+
|
187
|
+
free(wfmt);
|
188
|
+
list_elem_destroy(ap);
|
189
|
+
|
190
|
+
wfmt = formatted;
|
191
|
+
passes--;
|
192
|
+
}
|
193
|
+
|
194
|
+
final = wcstorbs(formatted);
|
195
|
+
|
196
|
+
free(formatted);
|
197
|
+
|
198
|
+
return final;
|
199
|
+
}
|
200
|
+
|
201
|
+
VALUE rb_alt_printf_single_pass(size_t argc, VALUE *argv, VALUE self) {
|
202
|
+
VALUE fmt, args, hash, final;
|
203
|
+
|
204
|
+
return rb_alt_printf(1, argc, argv, self);
|
205
|
+
}
|
206
|
+
|
207
|
+
VALUE rb_alt_printf_multi_pass(size_t argc, VALUE *argv, VALUE self) {
|
208
|
+
long passes;
|
209
|
+
|
210
|
+
passes = FIX2LONG(argv[0]);
|
211
|
+
|
212
|
+
LOG("passes: %ld\n", passes);
|
213
|
+
return rb_alt_printf(passes, argc - 1, &argv[1], self);
|
214
|
+
}
|
215
|
+
|
216
|
+
void Init_alt_printf()
|
217
|
+
{
|
218
|
+
enc = rb_enc_find("UTF-8");
|
219
|
+
VALUE mod = rb_define_module(MODNAME);
|
220
|
+
rb_define_module_function(mod, "fmt", rb_alt_printf_single_pass, -1);
|
221
|
+
rb_define_module_function(mod, "fmtm", rb_alt_printf_multi_pass, -1);
|
222
|
+
}
|
@@ -0,0 +1,244 @@
|
|
1
|
+
#include "syntax.h"
|
2
|
+
#include "altprintf.h"
|
3
|
+
#include "log.h"
|
4
|
+
|
5
|
+
void default_format(struct format *f) {
|
6
|
+
struct width width = {.prec = -1, .pad = 0};
|
7
|
+
f->stringarg_start = NULL;
|
8
|
+
f->stringarg_end = NULL;
|
9
|
+
f->chararg = L':';
|
10
|
+
f->padchar = L' ';
|
11
|
+
f->align = Right;
|
12
|
+
f->width = width;
|
13
|
+
f->le = NULL;
|
14
|
+
}
|
15
|
+
|
16
|
+
void format_mul(struct strbuf *sb, struct format *f)
|
17
|
+
{
|
18
|
+
long int *i = f->le->data;
|
19
|
+
strbuf_pad(sb, f->chararg, *i);
|
20
|
+
}
|
21
|
+
void format_tern(struct strbuf *sb, struct format *f)
|
22
|
+
{
|
23
|
+
long int *b = f->le->data;
|
24
|
+
int first_half = 1;
|
25
|
+
wchar_t sep = f->chararg;
|
26
|
+
wchar_t *p = f->stringarg_start;
|
27
|
+
for (;p<=f->stringarg_end;p++) {
|
28
|
+
LOG("*p: %lc, first half? %d, bool: %ld, sep: %lc\n", (wint_t)*p, first_half, *b, (wint_t)sep);
|
29
|
+
if (*p == sep) first_half = 0;
|
30
|
+
else if (*b && first_half) strbuf_append_char(sb, p);
|
31
|
+
else if (!*b && !first_half) strbuf_append_char(sb, p);
|
32
|
+
}
|
33
|
+
}
|
34
|
+
|
35
|
+
void format_string(struct strbuf *sb, struct format *f)
|
36
|
+
{
|
37
|
+
int prec = f->width.prec == -1 ? 100000000 : f->width.prec;
|
38
|
+
strbuf_append_str(sb, f->le->data, prec);
|
39
|
+
}
|
40
|
+
void format_char(struct strbuf *sb, struct format *f)
|
41
|
+
{
|
42
|
+
strbuf_append_char(sb, f->le->data);
|
43
|
+
}
|
44
|
+
void format_int(struct strbuf *sb, struct format *f)
|
45
|
+
{
|
46
|
+
strbuf_append_int(sb, f->le->data);
|
47
|
+
}
|
48
|
+
void format_double(struct strbuf *sb, struct format *f)
|
49
|
+
{
|
50
|
+
int prec = f->width.prec == -1 ? 3 : f->width.prec;
|
51
|
+
strbuf_append_double(sb, f->le->data, prec);
|
52
|
+
}
|
53
|
+
|
54
|
+
void format(struct strbuf *sb, struct format *f, void (*to_s)(struct strbuf *, struct format *))
|
55
|
+
{
|
56
|
+
struct strbuf *tmp = strbuf_new();
|
57
|
+
to_s(tmp, f);
|
58
|
+
|
59
|
+
if (tmp->len == 0) return;
|
60
|
+
|
61
|
+
int pad = f->width.pad - tmp->width;
|
62
|
+
|
63
|
+
if (pad > 0) {
|
64
|
+
LOG("padding: %d\n", pad);
|
65
|
+
switch(f->align) {
|
66
|
+
case Right:
|
67
|
+
strbuf_append_strbuf(sb, tmp);
|
68
|
+
strbuf_pad(sb, f->padchar, pad);
|
69
|
+
break;
|
70
|
+
case Left:
|
71
|
+
strbuf_pad(sb, f->padchar, pad);
|
72
|
+
strbuf_append_strbuf(sb, tmp);
|
73
|
+
break;
|
74
|
+
case Center:
|
75
|
+
strbuf_pad(sb, f->padchar, pad/2);
|
76
|
+
strbuf_append_strbuf(sb, tmp);
|
77
|
+
strbuf_pad(sb, f->padchar, pad/2 + pad%2);
|
78
|
+
break;
|
79
|
+
}
|
80
|
+
} else {
|
81
|
+
strbuf_append_strbuf(sb, tmp);
|
82
|
+
}
|
83
|
+
|
84
|
+
strbuf_destroy(tmp);
|
85
|
+
}
|
86
|
+
|
87
|
+
wchar_t *altsprintf(wchar_t *fmt, struct list_elem *le) {
|
88
|
+
int lvl = 0;
|
89
|
+
int split = 0;
|
90
|
+
struct strbuf *sbs[] = {strbuf_new(), strbuf_new()};
|
91
|
+
struct strbuf *sb = sbs[0];
|
92
|
+
wchar_t *end = &fmt[wcslen(fmt)];
|
93
|
+
wchar_t *jump;
|
94
|
+
|
95
|
+
void (*append_func)(struct strbuf *, struct format *);
|
96
|
+
struct format f;
|
97
|
+
long int *number_p = NULL;
|
98
|
+
long int *width;
|
99
|
+
wint_t split_pad = L' ';
|
100
|
+
|
101
|
+
for (;fmt<end;fmt++) {
|
102
|
+
LOG("checking char '%lc', lvl: '%d'\n", (wint_t)(*fmt), lvl);
|
103
|
+
switch (lvl) {
|
104
|
+
case 0:
|
105
|
+
switch(*fmt) {
|
106
|
+
case FS_START:
|
107
|
+
default_format(&f);
|
108
|
+
lvl = 1;
|
109
|
+
break;
|
110
|
+
case FS_ESC:
|
111
|
+
lvl = 3;
|
112
|
+
break;
|
113
|
+
default:
|
114
|
+
strbuf_append(sb, *fmt);
|
115
|
+
break;
|
116
|
+
}; break;
|
117
|
+
case 1:
|
118
|
+
switch(*fmt) {
|
119
|
+
/* special arguments */
|
120
|
+
case FS_A_STRINGSTART:
|
121
|
+
f.stringarg_start = fmt + 1;
|
122
|
+
lvl = 2;
|
123
|
+
break;
|
124
|
+
case FS_A_CHARARG:
|
125
|
+
f.chararg = *(fmt+1);
|
126
|
+
fmt += 1;
|
127
|
+
break;
|
128
|
+
|
129
|
+
/* standard arguments */
|
130
|
+
case FS_A_LALIGN:
|
131
|
+
f.align = Left;
|
132
|
+
break;
|
133
|
+
case FS_A_SPAD:
|
134
|
+
f.padchar = FS_A_SPAD;
|
135
|
+
break;
|
136
|
+
case 0:
|
137
|
+
f.padchar = '0';
|
138
|
+
break;
|
139
|
+
case '.':
|
140
|
+
number_p = &f.width.prec;
|
141
|
+
fmt++;
|
142
|
+
case '1': case '2': case '3': case '4': case '5':
|
143
|
+
case '6': case '7': case '8': case '9':
|
144
|
+
if (number_p == NULL) number_p = &f.width.pad;
|
145
|
+
*number_p = wcstol(fmt, &jump, 10);
|
146
|
+
fmt = (jump-1);
|
147
|
+
break;
|
148
|
+
|
149
|
+
/* align operator */
|
150
|
+
case FS_T_ALIGN:
|
151
|
+
if (le != NULL && le->type != Null) {
|
152
|
+
width = le->data;
|
153
|
+
le = le->next;
|
154
|
+
} else {
|
155
|
+
goto no_more_args;
|
156
|
+
}
|
157
|
+
|
158
|
+
split = 1;
|
159
|
+
split_pad = f.chararg;
|
160
|
+
sb = sbs[1];
|
161
|
+
lvl = 0;
|
162
|
+
break;
|
163
|
+
|
164
|
+
/* types */
|
165
|
+
case FS_T_STRING:
|
166
|
+
append_func = format_string;
|
167
|
+
goto match;
|
168
|
+
case FS_T_TERN:
|
169
|
+
append_func = format_tern;
|
170
|
+
goto match;
|
171
|
+
case FS_T_INT:
|
172
|
+
append_func = format_int;
|
173
|
+
goto match;
|
174
|
+
case FS_T_MUL:
|
175
|
+
append_func = format_mul;
|
176
|
+
goto match;
|
177
|
+
case FS_T_CHAR:
|
178
|
+
append_func = format_char;
|
179
|
+
goto match;
|
180
|
+
case FS_T_DOUBLE:
|
181
|
+
append_func = format_double;
|
182
|
+
match:
|
183
|
+
if (le != NULL && le->type != Null) {
|
184
|
+
f.le = le;
|
185
|
+
format(sb, &f, append_func);
|
186
|
+
le = le->next;
|
187
|
+
}
|
188
|
+
lvl = 0;
|
189
|
+
break;
|
190
|
+
case FS_START:
|
191
|
+
strbuf_append(sb, FS_START);
|
192
|
+
lvl = 0;
|
193
|
+
break;
|
194
|
+
}; break;
|
195
|
+
case 2:
|
196
|
+
if (*fmt == FS_A_STRINGEND) {
|
197
|
+
f.stringarg_end = fmt - 1;
|
198
|
+
lvl = 1;
|
199
|
+
}; break;
|
200
|
+
case 3:
|
201
|
+
switch(*fmt) {
|
202
|
+
case FS_ESC_NL:
|
203
|
+
strbuf_append(sb, '\n');
|
204
|
+
break;
|
205
|
+
case FS_ESC_ESC:
|
206
|
+
strbuf_append(sb, '\e');
|
207
|
+
break;
|
208
|
+
default:
|
209
|
+
strbuf_append(sb, *fmt);
|
210
|
+
break;
|
211
|
+
};
|
212
|
+
lvl = 0;
|
213
|
+
break;
|
214
|
+
}
|
215
|
+
}
|
216
|
+
|
217
|
+
wchar_t *str;
|
218
|
+
no_more_args:
|
219
|
+
if (split) {
|
220
|
+
LOG("splitting string\n");
|
221
|
+
lvl = *width - (sbs[0]->width + sbs[1]->width);
|
222
|
+
if (lvl >= 0) {
|
223
|
+
LOG("padding center\n");
|
224
|
+
strbuf_pad(sbs[0], split_pad, lvl);
|
225
|
+
strbuf_append_strbuf(sbs[0], sbs[1]);
|
226
|
+
} else if (sbs[0]->width > *width) {
|
227
|
+
LOG("the first half is longer than the requested with\n");
|
228
|
+
strbuf_destroy(sbs[1]);
|
229
|
+
sbs[1] = sbs[0];
|
230
|
+
sbs[0] = strbuf_new();
|
231
|
+
strbuf_appendw_strbuf(sbs[0], sbs[1], *width);
|
232
|
+
} else {
|
233
|
+
LOG("just shave some off the last half\n");
|
234
|
+
strbuf_appendw_strbuf(sbs[0], sbs[1], *width - sbs[0]->width);
|
235
|
+
}
|
236
|
+
}
|
237
|
+
|
238
|
+
str = strbuf_cstr(sbs[0]);
|
239
|
+
|
240
|
+
strbuf_destroy(sbs[0]);
|
241
|
+
strbuf_destroy(sbs[1]);
|
242
|
+
|
243
|
+
return str;
|
244
|
+
}
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#ifndef ALTPRINTF_H
|
2
|
+
#define ALTPRINTF_H
|
3
|
+
|
4
|
+
#ifndef _XOPEN_SOURCE
|
5
|
+
#define _XOPEN_SOURCE
|
6
|
+
#endif
|
7
|
+
|
8
|
+
#include <stdio.h>
|
9
|
+
#include <stdlib.h>
|
10
|
+
#include <stdarg.h>
|
11
|
+
#include <string.h>
|
12
|
+
#include <math.h>
|
13
|
+
#include <limits.h>
|
14
|
+
#include <wchar.h>
|
15
|
+
#include "strbuf.h"
|
16
|
+
#include "list.h"
|
17
|
+
#include "syntax.h"
|
18
|
+
|
19
|
+
wchar_t *altsprintf(wchar_t *fmt, struct list_elem *le);
|
20
|
+
|
21
|
+
enum align {
|
22
|
+
Left,
|
23
|
+
Right,
|
24
|
+
Center
|
25
|
+
};
|
26
|
+
|
27
|
+
struct width {
|
28
|
+
long int prec;
|
29
|
+
long int pad;
|
30
|
+
};
|
31
|
+
|
32
|
+
struct format {
|
33
|
+
wchar_t *stringarg_start;
|
34
|
+
wchar_t *stringarg_end;
|
35
|
+
wint_t chararg;
|
36
|
+
wint_t padchar;
|
37
|
+
enum align align;
|
38
|
+
struct width width;
|
39
|
+
struct list_elem *le;
|
40
|
+
};
|
41
|
+
|
42
|
+
#endif
|
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'mkmf'
|
3
|
+
|
4
|
+
base_dir = File.join(__dir__, '../../../')
|
5
|
+
find_header('altprintf.h', File.join(base_dir, 'src'))
|
6
|
+
$objs = Dir[File.join(base_dir, 'target/release/*.o')] + ['alt_printf.o']
|
7
|
+
|
8
|
+
create_header
|
9
|
+
create_makefile('alt_printf')
|
@@ -0,0 +1,81 @@
|
|
1
|
+
#include "list.h"
|
2
|
+
#include "log.h"
|
3
|
+
|
4
|
+
struct list_elem *list_elem_create() {
|
5
|
+
void *null = NULL;
|
6
|
+
struct list_elem *le = malloc(sizeof(struct list_elem));
|
7
|
+
LOG("allocatted %zu %p\n", sizeof(struct list_elem), le);
|
8
|
+
le->data = null;
|
9
|
+
le->type = Null;
|
10
|
+
le->next = null;
|
11
|
+
le->heap = 1;
|
12
|
+
|
13
|
+
return le;
|
14
|
+
}
|
15
|
+
|
16
|
+
void list_elem_destroy(struct list_elem *le) {
|
17
|
+
if (le->heap && le->data != NULL) {
|
18
|
+
LOG("freeing %p\n", le->data);
|
19
|
+
free(le->data);
|
20
|
+
}
|
21
|
+
if (le->next != NULL) { list_elem_destroy(le->next); }
|
22
|
+
|
23
|
+
LOG("freeing %p\n", le);
|
24
|
+
free(le);
|
25
|
+
}
|
26
|
+
|
27
|
+
struct list_elem *list_elem_ini(void *data, enum type t) {
|
28
|
+
struct list_elem *le = list_elem_create();
|
29
|
+
le->data = data;
|
30
|
+
le->type = t;
|
31
|
+
return le;
|
32
|
+
}
|
33
|
+
|
34
|
+
struct list_elem *list_elem_ini_str(wchar_t *str) {
|
35
|
+
struct list_elem *le = list_elem_create();
|
36
|
+
le->data = str;
|
37
|
+
le->type = String;
|
38
|
+
return le;
|
39
|
+
}
|
40
|
+
|
41
|
+
struct list_elem *list_elem_ini_int(long int *i) {
|
42
|
+
struct list_elem *le = list_elem_create();
|
43
|
+
le->data = i;
|
44
|
+
le->type = Int;
|
45
|
+
return le;
|
46
|
+
}
|
47
|
+
|
48
|
+
struct list_elem *list_elem_ini_dub(double *d) {
|
49
|
+
struct list_elem *le = list_elem_create();
|
50
|
+
le->data = d;
|
51
|
+
le->type = Double;
|
52
|
+
return le;
|
53
|
+
}
|
54
|
+
|
55
|
+
void list_elem_inspect(struct list_elem *le) {
|
56
|
+
switch (le->type) {
|
57
|
+
case Int:
|
58
|
+
printf("->Int %ld\n", *(long int *)le->data);
|
59
|
+
break;
|
60
|
+
case String:
|
61
|
+
printf("->String '%ls'\n", (wchar_t *)le->data);
|
62
|
+
break;
|
63
|
+
case Char:
|
64
|
+
printf("->Char %lc\n", *(wint_t *)le->data);
|
65
|
+
break;
|
66
|
+
case Double:
|
67
|
+
printf("->Double %f\n", *(double *)le->data);
|
68
|
+
break;
|
69
|
+
case Null:
|
70
|
+
printf("->Null\n");
|
71
|
+
break;
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
void list_elem_inspect_all(struct list_elem *le) {
|
76
|
+
while(1) {
|
77
|
+
list_elem_inspect(le);
|
78
|
+
if (le->next == NULL) { break; }
|
79
|
+
le = le->next;
|
80
|
+
}
|
81
|
+
}
|
@@ -0,0 +1,24 @@
|
|
1
|
+
#ifndef LIST_H
|
2
|
+
#define LIST_H
|
3
|
+
#include <stdio.h>
|
4
|
+
#include <stdlib.h>
|
5
|
+
#include <wchar.h>
|
6
|
+
|
7
|
+
enum type { Int, String, Char, Double, Null };
|
8
|
+
|
9
|
+
struct list_elem {
|
10
|
+
void *data;
|
11
|
+
enum type type;
|
12
|
+
struct list_elem *next;
|
13
|
+
int heap;
|
14
|
+
};
|
15
|
+
|
16
|
+
struct list_elem *list_elem_create();
|
17
|
+
void list_elem_destroy(struct list_elem *le);
|
18
|
+
struct list_elem *list_elem_ini(void *data, enum type t);
|
19
|
+
struct list_elem *list_elem_ini_int(long int *i);
|
20
|
+
struct list_elem *list_elem_ini_str(wchar_t *str);
|
21
|
+
struct list_elem *list_elem_ini_dub(double *d);
|
22
|
+
void list_elem_inspect(struct list_elem *le);
|
23
|
+
void list_elem_inspect_all(struct list_elem *le);
|
24
|
+
#endif
|
@@ -0,0 +1,11 @@
|
|
1
|
+
#ifndef LOG_H
|
2
|
+
#define LOG_H
|
3
|
+
|
4
|
+
#ifdef DEBUG
|
5
|
+
#define LOG(...) printf("%s[%d] - ", __FILE__, __LINE__);printf(__VA_ARGS__);
|
6
|
+
#define FLOG(msg, ...) fprintf(stderr, "%s[%d] - "msg, __FILE__, __LINE__ __VA_OPT__(,) __VA_ARGS__)
|
7
|
+
#else
|
8
|
+
#define LOG(msg, ...)
|
9
|
+
#define FLOG(msg, ...)
|
10
|
+
#endif
|
11
|
+
#endif
|
@@ -0,0 +1,142 @@
|
|
1
|
+
#define _XOPEN_SOURCE
|
2
|
+
#include <locale.h>
|
3
|
+
#include <limits.h>
|
4
|
+
#include "strbuf.h"
|
5
|
+
#include "log.h"
|
6
|
+
|
7
|
+
#define STRBUF_INI_SIZE 5
|
8
|
+
#define STRBUF_GROW_STEP 100
|
9
|
+
|
10
|
+
extern struct lconv *locale_info;
|
11
|
+
|
12
|
+
struct strbuf *strbuf_new() {
|
13
|
+
struct strbuf *sb = malloc(sizeof(struct strbuf));
|
14
|
+
|
15
|
+
if (NULL == sb) {
|
16
|
+
LOG("can't alloc memory for new strbuf", NULL);
|
17
|
+
exit(1);
|
18
|
+
}
|
19
|
+
|
20
|
+
sb->start = sb->end = calloc(STRBUF_INI_SIZE, sizeof(wchar_t));
|
21
|
+
|
22
|
+
if (NULL == sb->start) {
|
23
|
+
LOG("can't alloc memory for new strbuf string", NULL);
|
24
|
+
exit(1);
|
25
|
+
}
|
26
|
+
|
27
|
+
sb->len = 0;
|
28
|
+
sb->cap = STRBUF_INI_SIZE;
|
29
|
+
sb->width = 0;
|
30
|
+
return sb;
|
31
|
+
}
|
32
|
+
|
33
|
+
void strbuf_destroy(struct strbuf *sb) {
|
34
|
+
free(sb->start);
|
35
|
+
free(sb);
|
36
|
+
}
|
37
|
+
|
38
|
+
void strbuf_append(struct strbuf *sb, wchar_t c)
|
39
|
+
{
|
40
|
+
wchar_t *ns;
|
41
|
+
|
42
|
+
if (sb->cap < sb->len + 2) {
|
43
|
+
ns = calloc(sb->cap + STRBUF_GROW_STEP, sizeof(wchar_t));
|
44
|
+
|
45
|
+
if (ns == NULL) {
|
46
|
+
LOG("can't increase size of strbuf to %d\n", sb->cap + STRBUF_GROW_STEP);
|
47
|
+
exit(1);
|
48
|
+
}
|
49
|
+
|
50
|
+
wcscpy(ns, sb->start);
|
51
|
+
free(sb->start);
|
52
|
+
|
53
|
+
sb->start = ns;
|
54
|
+
sb->cap += STRBUF_GROW_STEP;
|
55
|
+
}
|
56
|
+
|
57
|
+
sb->start[sb->len] = c;
|
58
|
+
sb->start[sb->len+1] = L'\0';
|
59
|
+
LOG("string so far: %ls\n", sb->start);
|
60
|
+
sb->end = &sb->start[sb->len];
|
61
|
+
sb->len++;
|
62
|
+
int w = wcwidth(c);
|
63
|
+
if (w >= 0) sb->width += w;
|
64
|
+
}
|
65
|
+
|
66
|
+
void strbuf_append_strbuf(struct strbuf *sb, void *sbuf)
|
67
|
+
{
|
68
|
+
wchar_t *pos;
|
69
|
+
struct strbuf *frm = sbuf;
|
70
|
+
LOG("frm->start: %p | frm->end: %p\n", frm->start, frm->end);
|
71
|
+
|
72
|
+
for (pos = frm->start;pos<=frm->end;pos++) {
|
73
|
+
strbuf_append(sb, *pos);
|
74
|
+
}
|
75
|
+
}
|
76
|
+
|
77
|
+
void strbuf_appendw_strbuf(struct strbuf *sb, void *sbuf, long w)
|
78
|
+
{
|
79
|
+
wchar_t *pos;
|
80
|
+
long ws = 0;
|
81
|
+
struct strbuf *frm = sbuf;
|
82
|
+
LOG("frm->start: %p | frm->end: %p\n", frm->start, frm->end);
|
83
|
+
|
84
|
+
for (pos = frm->start;pos<=frm->end;pos++) {
|
85
|
+
ws += wcwidth(*pos);
|
86
|
+
LOG("new width would be: %ld, requested width: %ld\n", ws, w);
|
87
|
+
if (ws > w) break;
|
88
|
+
strbuf_append(sb, *pos);
|
89
|
+
}
|
90
|
+
}
|
91
|
+
|
92
|
+
void strbuf_append_char(struct strbuf *sb, void *chr)
|
93
|
+
{
|
94
|
+
wint_t *c = chr;
|
95
|
+
strbuf_append(sb, *c);
|
96
|
+
}
|
97
|
+
|
98
|
+
void strbuf_append_str(struct strbuf *sb, void *str, int maxwidth)
|
99
|
+
{
|
100
|
+
wchar_t *s = str;
|
101
|
+
wchar_t *end = &s[wcslen(s)];
|
102
|
+
int width = 0;
|
103
|
+
|
104
|
+
for (;s<end;s++) {
|
105
|
+
width += wcwidth(*s);
|
106
|
+
if (width > maxwidth) return;
|
107
|
+
strbuf_append(sb, *s);
|
108
|
+
}
|
109
|
+
}
|
110
|
+
|
111
|
+
void strbuf_append_int(struct strbuf *sb, void *in)
|
112
|
+
{
|
113
|
+
long int *i = in;
|
114
|
+
wchar_t wcs[50];
|
115
|
+
swprintf(wcs, 50, L"%ld", *i);
|
116
|
+
strbuf_append_str(sb, wcs, 50);
|
117
|
+
}
|
118
|
+
|
119
|
+
void strbuf_append_double(struct strbuf *sb, void *dub, int prec)
|
120
|
+
{
|
121
|
+
double *d = dub;
|
122
|
+
wchar_t wcs[50];
|
123
|
+
wchar_t format[30];
|
124
|
+
swprintf(format, 30, L"%%.%ldf", prec);
|
125
|
+
LOG("format: %ls\n", format);
|
126
|
+
swprintf(wcs, 50, format, *d);
|
127
|
+
strbuf_append_str(sb, wcs, 50);
|
128
|
+
}
|
129
|
+
|
130
|
+
void strbuf_pad(struct strbuf *sb, wchar_t pc, int amnt)
|
131
|
+
{
|
132
|
+
for (;amnt>0;amnt--) strbuf_append(sb, pc);
|
133
|
+
}
|
134
|
+
|
135
|
+
wchar_t *strbuf_cstr(struct strbuf *sb)
|
136
|
+
{
|
137
|
+
wchar_t *str;
|
138
|
+
str = calloc(sb->len + 1, sizeof(wchar_t));
|
139
|
+
wcscpy(str, sb->start);
|
140
|
+
|
141
|
+
return str;
|
142
|
+
}
|
@@ -0,0 +1,28 @@
|
|
1
|
+
#ifndef STRBUF_H
|
2
|
+
#define STRBUF_H
|
3
|
+
#include <wchar.h>
|
4
|
+
#include <stdio.h>
|
5
|
+
#include <stdlib.h>
|
6
|
+
#include <string.h>
|
7
|
+
#include <math.h>
|
8
|
+
|
9
|
+
struct strbuf *strbuf_new();
|
10
|
+
void strbuf_destroy(struct strbuf *sb);
|
11
|
+
void strbuf_append(struct strbuf *sb, wchar_t);
|
12
|
+
void strbuf_append_char(struct strbuf *sb, void *);
|
13
|
+
void strbuf_append_str(struct strbuf *, void *, int);
|
14
|
+
void strbuf_append_int(struct strbuf *sb, void *);
|
15
|
+
void strbuf_append_double(struct strbuf *, void *, int);
|
16
|
+
void strbuf_append_strbuf(struct strbuf *, void *);
|
17
|
+
void strbuf_appendw_strbuf(struct strbuf *, void *, long);
|
18
|
+
void strbuf_pad(struct strbuf *, wchar_t, int);
|
19
|
+
wchar_t *strbuf_cstr(struct strbuf *);
|
20
|
+
|
21
|
+
struct strbuf {
|
22
|
+
wchar_t *start;
|
23
|
+
wchar_t *end;
|
24
|
+
size_t len;
|
25
|
+
size_t width;
|
26
|
+
size_t cap;
|
27
|
+
};
|
28
|
+
#endif
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#ifndef SYNTAX_H
|
2
|
+
#define SYNTAX_H
|
3
|
+
|
4
|
+
#define FS_START '%'
|
5
|
+
#define FS_DOUBLE_SEP '.'
|
6
|
+
|
7
|
+
#define FS_ESC '\\'
|
8
|
+
#define FS_ESC_NL 'n'
|
9
|
+
#define FS_ESC_ESC 'e'
|
10
|
+
|
11
|
+
#define FS_T_STRING 's'
|
12
|
+
#define FS_T_CHAR 'c'
|
13
|
+
#define FS_T_DOUBLE 'f'
|
14
|
+
#define FS_T_INT 'd'
|
15
|
+
#define FS_T_MUL '*'
|
16
|
+
#define FS_T_TERN '?'
|
17
|
+
#define FS_T_ALIGN '='
|
18
|
+
|
19
|
+
#define FS_A_STRINGSTART '('
|
20
|
+
#define FS_A_STRINGEND ')'
|
21
|
+
#define FS_A_CHARARG '~'
|
22
|
+
#define FS_A_LALIGN '-'
|
23
|
+
#define FS_A_SPAD ' '
|
24
|
+
|
25
|
+
#define FS_A_RBHASHSTART '<'
|
26
|
+
#define FS_A_RBHASHEND '>'
|
27
|
+
|
28
|
+
#define FS_D_PREC 100
|
29
|
+
|
30
|
+
#endif
|
Binary file
|
data/lib/alt_printf.rb
ADDED
metadata
ADDED
@@ -0,0 +1,89 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: alt_printf
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Stone Tickle
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-08-07 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake-compiler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '12.3'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '12.3'
|
41
|
+
description:
|
42
|
+
email: lattis@mochiro.moe
|
43
|
+
executables: []
|
44
|
+
extensions:
|
45
|
+
- ext/alt_printf/extconf.rb
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- Gemfile
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- alt_printf.gemspec
|
52
|
+
- ext/alt_printf/alt_printf.c
|
53
|
+
- ext/alt_printf/altprintf.c
|
54
|
+
- ext/alt_printf/altprintf.h
|
55
|
+
- ext/alt_printf/extconf.rb
|
56
|
+
- ext/alt_printf/extconf_dev.rb
|
57
|
+
- ext/alt_printf/list.c
|
58
|
+
- ext/alt_printf/list.h
|
59
|
+
- ext/alt_printf/log.h
|
60
|
+
- ext/alt_printf/strbuf.c
|
61
|
+
- ext/alt_printf/strbuf.h
|
62
|
+
- ext/alt_printf/syntax.h
|
63
|
+
- lib/alt_printf.rb
|
64
|
+
- lib/alt_printf/alt_printf.so
|
65
|
+
- lib/alt_printf/version.rb
|
66
|
+
homepage: https://github.com/alt_printf/gem
|
67
|
+
licenses:
|
68
|
+
- MIT
|
69
|
+
metadata: {}
|
70
|
+
post_install_message:
|
71
|
+
rdoc_options: []
|
72
|
+
require_paths:
|
73
|
+
- lib
|
74
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: 2.6.3
|
79
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - ">="
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubygems_version: 3.0.3
|
86
|
+
signing_key:
|
87
|
+
specification_version: 4
|
88
|
+
summary: A powerful printf-like template language
|
89
|
+
test_files: []
|