glamour 0.0.1 → 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 +4 -4
- data/LICENSE.txt +21 -0
- data/README.md +283 -16
- data/ext/glamour/extconf.rb +59 -0
- data/ext/glamour/extension.c +300 -0
- data/glamour.gemspec +34 -0
- data/go/glamour.go +160 -0
- data/go/go.mod +35 -0
- data/go/go.sum +69 -0
- data/lib/glamour/renderer.rb +47 -0
- data/lib/glamour/style.rb +46 -0
- data/lib/glamour/style_definition.rb +39 -0
- data/lib/glamour/version.rb +4 -1
- data/lib/glamour.rb +70 -2
- metadata +22 -14
- data/CHANGELOG.md +0 -5
- data/CODE_OF_CONDUCT.md +0 -132
- data/Rakefile +0 -12
- data/sig/glamour.rbs +0 -4
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/* frozen_string_literal: true */
|
|
2
|
+
|
|
3
|
+
#include <ruby.h>
|
|
4
|
+
#include "libglamour.h"
|
|
5
|
+
|
|
6
|
+
VALUE mGlamour;
|
|
7
|
+
VALUE cRenderer;
|
|
8
|
+
|
|
9
|
+
#define COLOR_PROFILE_AUTO 0
|
|
10
|
+
#define COLOR_PROFILE_TRUE_COLOR 1
|
|
11
|
+
#define COLOR_PROFILE_ANSI256 2
|
|
12
|
+
#define COLOR_PROFILE_ANSI 3
|
|
13
|
+
#define COLOR_PROFILE_ASCII 4
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Render markdown to terminal-styled output.
|
|
17
|
+
*
|
|
18
|
+
* @param markdown [String] The markdown content to render
|
|
19
|
+
* @param style [String] Style name: "auto", "dark", "light", "notty", "dracula"
|
|
20
|
+
* @param width [Integer] Optional word wrap width
|
|
21
|
+
* @param emoji [Boolean] Enable emoji rendering
|
|
22
|
+
* @param preserve_newlines [Boolean] Preserve newlines in output
|
|
23
|
+
* @param base_url [String] Base URL for relative links
|
|
24
|
+
* @param color_profile [Symbol] Color profile: :auto, :true_color, :ansi256, :ansi, :ascii
|
|
25
|
+
* @return [String] Rendered output with ANSI escape codes
|
|
26
|
+
*/
|
|
27
|
+
static VALUE glamour_render_rb(int argc, VALUE *argv, VALUE self) {
|
|
28
|
+
VALUE markdown, options;
|
|
29
|
+
rb_scan_args(argc, argv, "1:", &markdown, &options);
|
|
30
|
+
|
|
31
|
+
Check_Type(markdown, T_STRING);
|
|
32
|
+
|
|
33
|
+
const char *style = "auto";
|
|
34
|
+
const char *base_url = NULL;
|
|
35
|
+
|
|
36
|
+
int width = 0;
|
|
37
|
+
int emoji = 0;
|
|
38
|
+
int preserve_newlines = 0;
|
|
39
|
+
int color_profile = COLOR_PROFILE_AUTO;
|
|
40
|
+
int has_advanced_options = 0;
|
|
41
|
+
|
|
42
|
+
if (!NIL_P(options)) {
|
|
43
|
+
VALUE style_value = rb_hash_lookup(options, ID2SYM(rb_intern("style")));
|
|
44
|
+
VALUE width_value = rb_hash_lookup(options, ID2SYM(rb_intern("width")));
|
|
45
|
+
VALUE emoji_value = rb_hash_lookup(options, ID2SYM(rb_intern("emoji")));
|
|
46
|
+
VALUE color_value = rb_hash_lookup(options, ID2SYM(rb_intern("color_profile")));
|
|
47
|
+
|
|
48
|
+
VALUE base_url_value = rb_hash_lookup(options, ID2SYM(rb_intern("base_url")));
|
|
49
|
+
VALUE preserve_value = rb_hash_lookup(options, ID2SYM(rb_intern("preserve_newlines")));
|
|
50
|
+
|
|
51
|
+
if (!NIL_P(style_value)) {
|
|
52
|
+
Check_Type(style_value, T_STRING);
|
|
53
|
+
style = StringValueCStr(style_value);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!NIL_P(width_value)) {
|
|
57
|
+
width = NUM2INT(width_value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (RTEST(emoji_value)) {
|
|
61
|
+
emoji = 1;
|
|
62
|
+
has_advanced_options = 1;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (RTEST(preserve_value)) {
|
|
66
|
+
preserve_newlines = 1;
|
|
67
|
+
has_advanced_options = 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!NIL_P(base_url_value)) {
|
|
71
|
+
Check_Type(base_url_value, T_STRING);
|
|
72
|
+
base_url = StringValueCStr(base_url_value);
|
|
73
|
+
has_advanced_options = 1;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!NIL_P(color_value)) {
|
|
77
|
+
has_advanced_options = 1;
|
|
78
|
+
|
|
79
|
+
if (color_value == ID2SYM(rb_intern("true_color"))) {
|
|
80
|
+
color_profile = COLOR_PROFILE_TRUE_COLOR;
|
|
81
|
+
} else if (color_value == ID2SYM(rb_intern("ansi256"))) {
|
|
82
|
+
color_profile = COLOR_PROFILE_ANSI256;
|
|
83
|
+
} else if (color_value == ID2SYM(rb_intern("ansi"))) {
|
|
84
|
+
color_profile = COLOR_PROFILE_ANSI;
|
|
85
|
+
} else if (color_value == ID2SYM(rb_intern("ascii"))) {
|
|
86
|
+
color_profile = COLOR_PROFILE_ASCII;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
char *result;
|
|
92
|
+
|
|
93
|
+
if (has_advanced_options || width > 0) {
|
|
94
|
+
result = glamour_render_with_options(
|
|
95
|
+
(char *) StringValueCStr(markdown),
|
|
96
|
+
(char *) style,
|
|
97
|
+
width,
|
|
98
|
+
emoji,
|
|
99
|
+
preserve_newlines,
|
|
100
|
+
(char *) base_url,
|
|
101
|
+
color_profile
|
|
102
|
+
);
|
|
103
|
+
} else {
|
|
104
|
+
result = glamour_render(
|
|
105
|
+
(char *) StringValueCStr(markdown),
|
|
106
|
+
(char *) style
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
VALUE rb_result = rb_utf8_str_new_cstr(result);
|
|
111
|
+
glamour_free(result);
|
|
112
|
+
|
|
113
|
+
return rb_result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/*
|
|
117
|
+
* Render markdown with a custom JSON style.
|
|
118
|
+
*
|
|
119
|
+
* @param markdown [String] The markdown content to render
|
|
120
|
+
* @param json_style [String] JSON style definition
|
|
121
|
+
* @param width [Integer] Optional word wrap width
|
|
122
|
+
* @return [String] Rendered output with ANSI escape codes
|
|
123
|
+
*/
|
|
124
|
+
static VALUE glamour_render_with_json_rb(int argc, VALUE *argv, VALUE self) {
|
|
125
|
+
VALUE markdown, json_style, options;
|
|
126
|
+
rb_scan_args(argc, argv, "2:", &markdown, &json_style, &options);
|
|
127
|
+
|
|
128
|
+
Check_Type(markdown, T_STRING);
|
|
129
|
+
Check_Type(json_style, T_STRING);
|
|
130
|
+
|
|
131
|
+
int width = 0;
|
|
132
|
+
|
|
133
|
+
if (!NIL_P(options)) {
|
|
134
|
+
VALUE width_value = rb_hash_lookup(options, ID2SYM(rb_intern("width")));
|
|
135
|
+
|
|
136
|
+
if (!NIL_P(width_value)) {
|
|
137
|
+
width = NUM2INT(width_value);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
char *result = glamour_render_with_json_style(
|
|
142
|
+
(char *) StringValueCStr(markdown),
|
|
143
|
+
(char *) StringValueCStr(json_style),
|
|
144
|
+
width
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
VALUE rb_result = rb_utf8_str_new_cstr(result);
|
|
148
|
+
glamour_free(result);
|
|
149
|
+
|
|
150
|
+
return rb_result;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* Get the upstream glamour version.
|
|
155
|
+
*
|
|
156
|
+
* @return [String] Upstream glamour version
|
|
157
|
+
*/
|
|
158
|
+
static VALUE glamour_upstream_version_rb(VALUE self) {
|
|
159
|
+
char *version = glamour_upstream_version();
|
|
160
|
+
VALUE rb_version = rb_utf8_str_new_cstr(version);
|
|
161
|
+
glamour_free(version);
|
|
162
|
+
|
|
163
|
+
return rb_version;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/*
|
|
167
|
+
* Get the glamour version info.
|
|
168
|
+
*
|
|
169
|
+
* @return [String] Version information string
|
|
170
|
+
*/
|
|
171
|
+
static VALUE glamour_version_rb(VALUE self) {
|
|
172
|
+
VALUE gem_version = rb_const_get(self, rb_intern("VERSION"));
|
|
173
|
+
VALUE upstream_version = glamour_upstream_version_rb(self);
|
|
174
|
+
VALUE format_string = rb_utf8_str_new_cstr("glamour v%s (upstream v%s) [Go native extension]");
|
|
175
|
+
|
|
176
|
+
return rb_funcall(rb_mKernel, rb_intern("sprintf"), 3, format_string, gem_version, upstream_version);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/* Renderer class methods */
|
|
180
|
+
|
|
181
|
+
static VALUE renderer_alloc(VALUE klass) {
|
|
182
|
+
return Data_Wrap_Struct(klass, NULL, NULL, NULL);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
static VALUE renderer_initialize(int argc, VALUE *argv, VALUE self) {
|
|
186
|
+
VALUE options;
|
|
187
|
+
rb_scan_args(argc, argv, "0:", &options);
|
|
188
|
+
|
|
189
|
+
rb_iv_set(self, "@style", rb_str_new_cstr("auto"));
|
|
190
|
+
rb_iv_set(self, "@width", INT2FIX(0));
|
|
191
|
+
rb_iv_set(self, "@emoji", Qfalse);
|
|
192
|
+
rb_iv_set(self, "@preserve_newlines", Qfalse);
|
|
193
|
+
rb_iv_set(self, "@base_url", Qnil);
|
|
194
|
+
rb_iv_set(self, "@color_profile", ID2SYM(rb_intern("auto")));
|
|
195
|
+
rb_iv_set(self, "@json_style", Qnil);
|
|
196
|
+
|
|
197
|
+
if (!NIL_P(options)) {
|
|
198
|
+
VALUE style = rb_hash_lookup(options, ID2SYM(rb_intern("style")));
|
|
199
|
+
if (!NIL_P(style)) rb_iv_set(self, "@style", style);
|
|
200
|
+
|
|
201
|
+
VALUE width = rb_hash_lookup(options, ID2SYM(rb_intern("width")));
|
|
202
|
+
if (!NIL_P(width)) rb_iv_set(self, "@width", width);
|
|
203
|
+
|
|
204
|
+
VALUE emoji = rb_hash_lookup(options, ID2SYM(rb_intern("emoji")));
|
|
205
|
+
if (RTEST(emoji)) rb_iv_set(self, "@emoji", Qtrue);
|
|
206
|
+
|
|
207
|
+
VALUE preserve = rb_hash_lookup(options, ID2SYM(rb_intern("preserve_newlines")));
|
|
208
|
+
if (RTEST(preserve)) rb_iv_set(self, "@preserve_newlines", Qtrue);
|
|
209
|
+
|
|
210
|
+
VALUE base_url = rb_hash_lookup(options, ID2SYM(rb_intern("base_url")));
|
|
211
|
+
if (!NIL_P(base_url)) rb_iv_set(self, "@base_url", base_url);
|
|
212
|
+
|
|
213
|
+
VALUE color = rb_hash_lookup(options, ID2SYM(rb_intern("color_profile")));
|
|
214
|
+
if (!NIL_P(color)) rb_iv_set(self, "@color_profile", color);
|
|
215
|
+
|
|
216
|
+
VALUE json = rb_hash_lookup(options, ID2SYM(rb_intern("json_style")));
|
|
217
|
+
if (!NIL_P(json)) rb_iv_set(self, "@json_style", json);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return self;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static VALUE renderer_render(VALUE self, VALUE markdown) {
|
|
224
|
+
Check_Type(markdown, T_STRING);
|
|
225
|
+
|
|
226
|
+
VALUE json_style = rb_iv_get(self, "@json_style");
|
|
227
|
+
|
|
228
|
+
if (!NIL_P(json_style)) {
|
|
229
|
+
int width = NUM2INT(rb_iv_get(self, "@width"));
|
|
230
|
+
|
|
231
|
+
char *result = glamour_render_with_json_style(
|
|
232
|
+
(char *)StringValueCStr(markdown),
|
|
233
|
+
(char *)StringValueCStr(json_style),
|
|
234
|
+
width
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
VALUE rb_result = rb_utf8_str_new_cstr(result);
|
|
238
|
+
glamour_free(result);
|
|
239
|
+
return rb_result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
VALUE style_value = rb_iv_get(self, "@style");
|
|
243
|
+
const char *style = NIL_P(style_value) ? "auto" : StringValueCStr(style_value);
|
|
244
|
+
int width = NUM2INT(rb_iv_get(self, "@width"));
|
|
245
|
+
int emoji = RTEST(rb_iv_get(self, "@emoji")) ? 1 : 0;
|
|
246
|
+
int preserve_newlines = RTEST(rb_iv_get(self, "@preserve_newlines")) ? 1 : 0;
|
|
247
|
+
|
|
248
|
+
VALUE base_url_value = rb_iv_get(self, "@base_url");
|
|
249
|
+
const char *base_url = NIL_P(base_url_value) ? NULL : StringValueCStr(base_url_value);
|
|
250
|
+
|
|
251
|
+
VALUE color_value = rb_iv_get(self, "@color_profile");
|
|
252
|
+
int color_profile = COLOR_PROFILE_AUTO;
|
|
253
|
+
|
|
254
|
+
if (color_value == ID2SYM(rb_intern("true_color"))) {
|
|
255
|
+
color_profile = COLOR_PROFILE_TRUE_COLOR;
|
|
256
|
+
} else if (color_value == ID2SYM(rb_intern("ansi256"))) {
|
|
257
|
+
color_profile = COLOR_PROFILE_ANSI256;
|
|
258
|
+
} else if (color_value == ID2SYM(rb_intern("ansi"))) {
|
|
259
|
+
color_profile = COLOR_PROFILE_ANSI;
|
|
260
|
+
} else if (color_value == ID2SYM(rb_intern("ascii"))) {
|
|
261
|
+
color_profile = COLOR_PROFILE_ASCII;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
char *result = glamour_render_with_options(
|
|
265
|
+
(char *)StringValueCStr(markdown),
|
|
266
|
+
(char *)style,
|
|
267
|
+
width,
|
|
268
|
+
emoji,
|
|
269
|
+
preserve_newlines,
|
|
270
|
+
(char *)base_url,
|
|
271
|
+
color_profile
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
VALUE rb_result = rb_utf8_str_new_cstr(result);
|
|
275
|
+
glamour_free(result);
|
|
276
|
+
|
|
277
|
+
return rb_result;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
__attribute__((__visibility__("default"))) void Init_glamour(void) {
|
|
281
|
+
mGlamour = rb_define_module("Glamour");
|
|
282
|
+
|
|
283
|
+
rb_define_singleton_method(mGlamour, "render", glamour_render_rb, -1);
|
|
284
|
+
rb_define_singleton_method(mGlamour, "render_with_json", glamour_render_with_json_rb, -1);
|
|
285
|
+
rb_define_singleton_method(mGlamour, "upstream_version", glamour_upstream_version_rb, 0);
|
|
286
|
+
rb_define_singleton_method(mGlamour, "version", glamour_version_rb, 0);
|
|
287
|
+
|
|
288
|
+
cRenderer = rb_define_class_under(mGlamour, "Renderer", rb_cObject);
|
|
289
|
+
rb_define_alloc_func(cRenderer, renderer_alloc);
|
|
290
|
+
rb_define_method(cRenderer, "initialize", renderer_initialize, -1);
|
|
291
|
+
rb_define_method(cRenderer, "render", renderer_render, 1);
|
|
292
|
+
|
|
293
|
+
rb_define_attr(cRenderer, "style", 1, 1);
|
|
294
|
+
rb_define_attr(cRenderer, "width", 1, 1);
|
|
295
|
+
rb_define_attr(cRenderer, "emoji", 1, 1);
|
|
296
|
+
rb_define_attr(cRenderer, "preserve_newlines", 1, 1);
|
|
297
|
+
rb_define_attr(cRenderer, "base_url", 1, 1);
|
|
298
|
+
rb_define_attr(cRenderer, "color_profile", 1, 1);
|
|
299
|
+
rb_define_attr(cRenderer, "json_style", 1, 1);
|
|
300
|
+
}
|
data/glamour.gemspec
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "lib/glamour/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "glamour"
|
|
7
|
+
spec.version = Glamour::VERSION
|
|
8
|
+
spec.authors = ["Marco Roth"]
|
|
9
|
+
spec.email = ["marco.roth@intergga.ch"]
|
|
10
|
+
|
|
11
|
+
spec.summary = "Ruby wrapper for Charm's glamour stylesheet-based markdown rendering for Ruby CLI apps."
|
|
12
|
+
spec.description = spec.summary
|
|
13
|
+
spec.homepage = "https://github.com/marcoroth/glamour-ruby"
|
|
14
|
+
spec.license = "MIT"
|
|
15
|
+
spec.required_ruby_version = ">= 3.2.0"
|
|
16
|
+
|
|
17
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
18
|
+
spec.metadata["source_code_uri"] = "https://github.com/marcoroth/glamour-ruby"
|
|
19
|
+
spec.metadata["changelog_uri"] = "https://github.com/marcoroth/glamour-ruby/releases"
|
|
20
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
21
|
+
|
|
22
|
+
spec.files = Dir[
|
|
23
|
+
"glamour.gemspec",
|
|
24
|
+
"LICENSE.txt",
|
|
25
|
+
"README.md",
|
|
26
|
+
"lib/**/*.rb",
|
|
27
|
+
"ext/**/*.{c,h,rb}",
|
|
28
|
+
"go/**/*.{go,mod,sum}",
|
|
29
|
+
"go/build/**/*"
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
spec.require_paths = ["lib"]
|
|
33
|
+
spec.extensions = ["ext/glamour/extconf.rb"]
|
|
34
|
+
end
|
data/go/glamour.go
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
#include <stdlib.h>
|
|
5
|
+
*/
|
|
6
|
+
import "C"
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"runtime/debug"
|
|
10
|
+
"unsafe"
|
|
11
|
+
|
|
12
|
+
"github.com/charmbracelet/glamour"
|
|
13
|
+
"github.com/muesli/termenv"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
//export glamour_render
|
|
17
|
+
func glamour_render(markdown *C.char, style *C.char) *C.char {
|
|
18
|
+
result, err := glamour.Render(C.GoString(markdown), C.GoString(style))
|
|
19
|
+
|
|
20
|
+
if err != nil {
|
|
21
|
+
return C.CString("")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return C.CString(result)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
//export glamour_render_with_width
|
|
28
|
+
func glamour_render_with_width(markdown *C.char, style *C.char, width C.int) *C.char {
|
|
29
|
+
renderer, err := glamour.NewTermRenderer(
|
|
30
|
+
glamour.WithStylePath(C.GoString(style)),
|
|
31
|
+
glamour.WithWordWrap(int(width)),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if err != nil {
|
|
35
|
+
return C.CString("")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
result, err := renderer.Render(C.GoString(markdown))
|
|
39
|
+
|
|
40
|
+
if err != nil {
|
|
41
|
+
return C.CString("")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return C.CString(result)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//export glamour_render_with_options
|
|
48
|
+
func glamour_render_with_options(
|
|
49
|
+
markdown *C.char,
|
|
50
|
+
style *C.char,
|
|
51
|
+
width C.int,
|
|
52
|
+
emoji C.int,
|
|
53
|
+
preserveNewlines C.int,
|
|
54
|
+
baseURL *C.char,
|
|
55
|
+
colorProfile C.int,
|
|
56
|
+
) *C.char {
|
|
57
|
+
var options []glamour.TermRendererOption
|
|
58
|
+
|
|
59
|
+
styleStr := C.GoString(style)
|
|
60
|
+
|
|
61
|
+
if styleStr != "" {
|
|
62
|
+
options = append(options, glamour.WithStylePath(styleStr))
|
|
63
|
+
} else {
|
|
64
|
+
options = append(options, glamour.WithAutoStyle())
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if width > 0 {
|
|
68
|
+
options = append(options, glamour.WithWordWrap(int(width)))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if emoji != 0 {
|
|
72
|
+
options = append(options, glamour.WithEmoji())
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if preserveNewlines != 0 {
|
|
76
|
+
options = append(options, glamour.WithPreservedNewLines())
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if baseURL != nil {
|
|
80
|
+
baseURLStr := C.GoString(baseURL)
|
|
81
|
+
|
|
82
|
+
if baseURLStr != "" {
|
|
83
|
+
options = append(options, glamour.WithBaseURL(baseURLStr))
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 0=auto, 1=TrueColor, 2=ANSI256, 3=ANSI, 4=Ascii
|
|
88
|
+
switch colorProfile {
|
|
89
|
+
case 1:
|
|
90
|
+
options = append(options, glamour.WithColorProfile(termenv.TrueColor))
|
|
91
|
+
case 2:
|
|
92
|
+
options = append(options, glamour.WithColorProfile(termenv.ANSI256))
|
|
93
|
+
case 3:
|
|
94
|
+
options = append(options, glamour.WithColorProfile(termenv.ANSI))
|
|
95
|
+
case 4:
|
|
96
|
+
options = append(options, glamour.WithColorProfile(termenv.Ascii))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
renderer, err := glamour.NewTermRenderer(options...)
|
|
100
|
+
|
|
101
|
+
if err != nil {
|
|
102
|
+
return C.CString("")
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
result, err := renderer.Render(C.GoString(markdown))
|
|
106
|
+
|
|
107
|
+
if err != nil {
|
|
108
|
+
return C.CString("")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return C.CString(result)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//export glamour_render_with_json_style
|
|
115
|
+
func glamour_render_with_json_style(markdown *C.char, jsonStyle *C.char, width C.int) *C.char {
|
|
116
|
+
var options []glamour.TermRendererOption
|
|
117
|
+
|
|
118
|
+
jsonBytes := []byte(C.GoString(jsonStyle))
|
|
119
|
+
options = append(options, glamour.WithStylesFromJSONBytes(jsonBytes))
|
|
120
|
+
|
|
121
|
+
if width > 0 {
|
|
122
|
+
options = append(options, glamour.WithWordWrap(int(width)))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
renderer, err := glamour.NewTermRenderer(options...)
|
|
126
|
+
|
|
127
|
+
if err != nil {
|
|
128
|
+
return C.CString("")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
result, err := renderer.Render(C.GoString(markdown))
|
|
132
|
+
if err != nil {
|
|
133
|
+
return C.CString("")
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return C.CString(result)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
//export glamour_free
|
|
140
|
+
func glamour_free(ptr *C.char) {
|
|
141
|
+
C.free(unsafe.Pointer(ptr))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//export glamour_upstream_version
|
|
145
|
+
func glamour_upstream_version() *C.char {
|
|
146
|
+
info, ok := debug.ReadBuildInfo()
|
|
147
|
+
if !ok {
|
|
148
|
+
return C.CString("unknown")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for _, dep := range info.Deps {
|
|
152
|
+
if dep.Path == "github.com/charmbracelet/glamour" {
|
|
153
|
+
return C.CString(dep.Version)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return C.CString("unknown")
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func main() {}
|
data/go/go.mod
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module github.com/marcoroth/glamour-ruby/go
|
|
2
|
+
|
|
3
|
+
go 1.23.0
|
|
4
|
+
|
|
5
|
+
toolchain go1.24.6
|
|
6
|
+
|
|
7
|
+
require github.com/charmbracelet/glamour v0.10.0
|
|
8
|
+
|
|
9
|
+
require (
|
|
10
|
+
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
|
|
11
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
12
|
+
github.com/aymerick/douceur v0.2.0 // indirect
|
|
13
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
14
|
+
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect
|
|
15
|
+
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
16
|
+
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
|
17
|
+
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
|
|
18
|
+
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
19
|
+
github.com/dlclark/regexp2 v1.11.0 // indirect
|
|
20
|
+
github.com/gorilla/css v1.0.1 // indirect
|
|
21
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
22
|
+
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
23
|
+
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
24
|
+
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
|
25
|
+
github.com/muesli/reflow v0.3.0 // indirect
|
|
26
|
+
github.com/muesli/termenv v0.16.0 // indirect
|
|
27
|
+
github.com/rivo/uniseg v0.4.7 // indirect
|
|
28
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
29
|
+
github.com/yuin/goldmark v1.7.8 // indirect
|
|
30
|
+
github.com/yuin/goldmark-emoji v1.0.5 // indirect
|
|
31
|
+
golang.org/x/net v0.33.0 // indirect
|
|
32
|
+
golang.org/x/sys v0.32.0 // indirect
|
|
33
|
+
golang.org/x/term v0.31.0 // indirect
|
|
34
|
+
golang.org/x/text v0.24.0 // indirect
|
|
35
|
+
)
|
data/go/go.sum
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
github.com/alecthomas/assert/v2 v2.7.0 h1:QtqSACNS3tF7oasA8CU6A6sXZSBDqnm7RfpLl9bZqbE=
|
|
2
|
+
github.com/alecthomas/assert/v2 v2.7.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
|
3
|
+
github.com/alecthomas/chroma/v2 v2.14.0 h1:R3+wzpnUArGcQz7fCETQBzO5n9IMNi13iIs46aU4V9E=
|
|
4
|
+
github.com/alecthomas/chroma/v2 v2.14.0/go.mod h1:QolEbTfmUHIMVpBqxeDnNBj2uoeI4EbYP4i6n68SG4I=
|
|
5
|
+
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
|
|
6
|
+
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
|
7
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
8
|
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
9
|
+
github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8=
|
|
10
|
+
github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA=
|
|
11
|
+
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
|
12
|
+
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
|
13
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
14
|
+
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
15
|
+
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
|
16
|
+
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
|
17
|
+
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
|
18
|
+
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834/go.mod h1:aKC/t2arECF6rNOnaKaVU6y4t4ZeHQzqfxedE/VkVhA=
|
|
19
|
+
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
|
20
|
+
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
|
21
|
+
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
|
22
|
+
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
23
|
+
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
|
24
|
+
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
|
25
|
+
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf h1:rLG0Yb6MQSDKdB52aGX55JT1oi0P0Kuaj7wi1bLUpnI=
|
|
26
|
+
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf/go.mod h1:B3UgsnsBZS/eX42BlaNiJkD1pPOUa+oF1IYC6Yd2CEU=
|
|
27
|
+
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
28
|
+
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
29
|
+
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
|
|
30
|
+
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
|
31
|
+
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
|
32
|
+
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
|
33
|
+
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
|
34
|
+
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
|
35
|
+
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
36
|
+
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
37
|
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
|
38
|
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
|
39
|
+
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
|
40
|
+
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
41
|
+
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
42
|
+
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
|
43
|
+
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
|
44
|
+
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
|
|
45
|
+
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
|
|
46
|
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
47
|
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
48
|
+
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
49
|
+
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
|
50
|
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
51
|
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
52
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
53
|
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
54
|
+
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
|
55
|
+
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
|
56
|
+
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
|
57
|
+
github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
|
|
58
|
+
github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
|
|
59
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
|
60
|
+
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
|
61
|
+
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
|
62
|
+
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
|
63
|
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
64
|
+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
|
65
|
+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
66
|
+
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
|
67
|
+
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
|
68
|
+
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
|
69
|
+
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
module Glamour
|
|
6
|
+
class Renderer
|
|
7
|
+
# @rbs hash: Hash[Symbol, untyped] -- style definition hash
|
|
8
|
+
# @rbs return: String
|
|
9
|
+
def style_hash=(hash)
|
|
10
|
+
@json_style = JSON.generate(hash)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
alias original_initialize initialize
|
|
14
|
+
|
|
15
|
+
# @rbs style: String | singleton(Glamour::Style) -- style name or Style subclass
|
|
16
|
+
# @rbs width: Integer -- optional word wrap width
|
|
17
|
+
# @rbs emoji: bool -- whether to render emoji
|
|
18
|
+
# @rbs preserve_newlines: bool -- whether to preserve newlines
|
|
19
|
+
# @rbs base_url: String? -- base URL for relative links
|
|
20
|
+
# @rbs color_profile: Symbol -- color profile to use
|
|
21
|
+
# @rbs json_style: String? -- JSON style definition
|
|
22
|
+
# @rbs style_hash: Hash[Symbol, untyped]? -- style definition hash
|
|
23
|
+
# @rbs return: void
|
|
24
|
+
def initialize(style: "auto", width: 0, emoji: false, preserve_newlines: false, base_url: nil,
|
|
25
|
+
color_profile: :auto, json_style: nil, style_hash: nil)
|
|
26
|
+
actual_style = style
|
|
27
|
+
actual_json_style = json_style
|
|
28
|
+
|
|
29
|
+
if style.is_a?(Class) && style.respond_to?(:glamour_style?) && style.glamour_style?
|
|
30
|
+
actual_style = "auto"
|
|
31
|
+
actual_json_style = style.to_json unless style.to_h.empty?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
actual_json_style = JSON.generate(style_hash) if style_hash
|
|
35
|
+
|
|
36
|
+
original_initialize(
|
|
37
|
+
style: actual_style,
|
|
38
|
+
width: width,
|
|
39
|
+
emoji: emoji,
|
|
40
|
+
preserve_newlines: preserve_newlines,
|
|
41
|
+
base_url: base_url,
|
|
42
|
+
color_profile: color_profile,
|
|
43
|
+
json_style: actual_json_style
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# rbs_inline: enabled
|
|
4
|
+
|
|
5
|
+
require_relative "style_definition"
|
|
6
|
+
|
|
7
|
+
module Glamour
|
|
8
|
+
class Style
|
|
9
|
+
class << self
|
|
10
|
+
# @rbs @styles: Hash[Symbol, Hash[Symbol, untyped]]
|
|
11
|
+
|
|
12
|
+
# @rbs element: Symbol | String -- the element to style
|
|
13
|
+
# @rbs return: Hash[Symbol, untyped] -- the style definition
|
|
14
|
+
def style(element, &)
|
|
15
|
+
styles[element.to_sym] = StyleDefinition.new(&).to_h
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @rbs return: Hash[Symbol, Hash[Symbol, untyped]]
|
|
19
|
+
def styles
|
|
20
|
+
@styles ||= {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs return: Hash[String, Hash[Symbol, untyped]]
|
|
24
|
+
def to_h
|
|
25
|
+
styles.transform_keys(&:to_s)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# @rbs return: String
|
|
29
|
+
def to_json(*_args)
|
|
30
|
+
JSON.generate(to_h)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @rbs markdown: String -- the markdown content to render
|
|
34
|
+
# @rbs width: Integer -- optional word wrap width
|
|
35
|
+
# @rbs return: String -- rendered output with ANSI escape codes
|
|
36
|
+
def render(markdown, width: 0, **)
|
|
37
|
+
Glamour.render(markdown, style: self, width: width, **)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @rbs return: true
|
|
41
|
+
def glamour_style?
|
|
42
|
+
true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|