actionview-svelte-handler 0.2.0 → 0.4.1
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/.forgejo/workflows/javascript_police.yml +22 -0
- data/.forgejo/workflows/ruby_police.yml +27 -0
- data/.gitignore +14 -0
- data/.ruby-version +1 -0
- data/Gemfile +14 -0
- data/Gemfile.lock +284 -0
- data/README.md +113 -17
- data/Steepfile +12 -0
- data/actionview-svelte-handler.gemspec +21 -0
- data/bin/test +5 -0
- data/build.esbuild.js +22 -0
- data/lib/generators/svelte/install_generator.rb +13 -0
- data/lib/generators/svelte/templates/install/initializer.rb +17 -0
- data/lib/svelte/errors.rb +151 -0
- data/lib/svelte/handler.rb +53 -41
- data/lib/svelte/helpers.rb +13 -3
- data/lib/svelte/js/.DO_NOT_MODIFY.md +2 -0
- data/lib/svelte/js/builder.js +122 -0
- data/lib/svelte/railtie.rb +5 -1
- data/lib/svelte/templates/assembler.js.erb +16 -0
- data/lib/svelte/templates/island.html.erb +20 -0
- data/lib/svelte/variabilization.rb +11 -0
- data/lib/svelte/version.rb +1 -1
- data/lib/svelte.rb +31 -2
- data/lib/tasks/svelte_tasks.rake +4 -0
- data/lib/ts/builder.ts +174 -0
- data/lib/ts/types/.DO_NOT_MODIFY.md +2 -0
- data/lib/ts/types/builder.d.ts +42 -0
- data/package-lock.json +6643 -0
- data/package.json +39 -0
- data/rbs_collection.lock.yaml +344 -0
- data/rbs_collection.yaml +18 -0
- data/sig/lib/svelte/errors.rbs +38 -0
- data/sig/lib/svelte/handler.rbs +9 -0
- data/sig/lib/svelte/helpers.rbs +7 -0
- data/sig/lib/svelte/version.rbs +3 -0
- data/sig/lib/svelte.rbs +19 -0
- data/svelte-on-rails.png +0 -0
- data/tsconfig.json +15 -0
- data/watch.esbuild.js +4 -0
- metadata +37 -3
@@ -0,0 +1,151 @@
|
|
1
|
+
require "active_support/syntax_error_proxy"
|
2
|
+
require "action_view/template/error"
|
3
|
+
module Svelte
|
4
|
+
class CompilerError < ActiveSupport::SyntaxErrorProxy # steep:ignore UnknownConstant
|
5
|
+
attr_accessor :location, :suggestion, :template
|
6
|
+
|
7
|
+
def initialize(message, location)
|
8
|
+
@location = location
|
9
|
+
@sugggestion = @location.dig(:suggestion) || ""
|
10
|
+
# steep:ignore:start
|
11
|
+
super(message) # : self
|
12
|
+
# steep:ignore:end
|
13
|
+
end
|
14
|
+
|
15
|
+
def message
|
16
|
+
to_s
|
17
|
+
end
|
18
|
+
|
19
|
+
def backtrace
|
20
|
+
if suggestion
|
21
|
+
["#{Rails.root.join location[:file]}:#{location[:line]}:#{message}, #{suggestion}"] + caller
|
22
|
+
else
|
23
|
+
["#{Rails.root.join location[:file]}:#{location[:line]}:#{message}"] + caller
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def cause
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
|
31
|
+
def bindings
|
32
|
+
[template.send(:binding)]
|
33
|
+
end
|
34
|
+
|
35
|
+
def backtrace_locations
|
36
|
+
traces = backtrace.map { |trace|
|
37
|
+
file, line = trace.match(/^(.+?):(\d+).*$/, &:captures) || trace
|
38
|
+
ActiveSupport::SyntaxErrorProxy::BacktraceLocation.new(file, line.to_i, trace) # steep:ignore UnknownConstant
|
39
|
+
}
|
40
|
+
|
41
|
+
traces.map { |loc| ActiveSupport::SyntaxErrorProxy::BacktraceLocationProxy.new(loc, self) } # steep:ignore UnknownConstant
|
42
|
+
end
|
43
|
+
|
44
|
+
def annotated_source_code
|
45
|
+
location[:lineText].split("\n").map.with_index(1) { |line, index|
|
46
|
+
indentation = " " * 4
|
47
|
+
"#{index}:#{indentation}#{line}"
|
48
|
+
}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class TemplateError < StandardError
|
53
|
+
attr_reader :cause, :template
|
54
|
+
|
55
|
+
SOURCE_CODE_RADIUS = 3
|
56
|
+
|
57
|
+
def initialize(template, error)
|
58
|
+
raise("Did not provide cause error") unless error
|
59
|
+
@cause = error
|
60
|
+
|
61
|
+
raise("Did not provide template") unless template
|
62
|
+
@template, @sub_templates = template, nil
|
63
|
+
|
64
|
+
raise("Cause error is nil for TemplateError") unless @cause
|
65
|
+
@cause.template = template
|
66
|
+
|
67
|
+
# steep:ignore:start
|
68
|
+
super(@cause.message) # : self
|
69
|
+
# steep:ignore:end
|
70
|
+
end
|
71
|
+
|
72
|
+
def message
|
73
|
+
@cause.message
|
74
|
+
end
|
75
|
+
|
76
|
+
def annotated_source_code
|
77
|
+
@cause.annotated_source_code
|
78
|
+
end
|
79
|
+
|
80
|
+
# Following is copypasta-ed from ActionView::Template::Error
|
81
|
+
def backtrace
|
82
|
+
@cause.backtrace
|
83
|
+
end
|
84
|
+
|
85
|
+
def backtrace_locations
|
86
|
+
@cause.backtrace_locations
|
87
|
+
end
|
88
|
+
|
89
|
+
def file_name
|
90
|
+
@template.identifier
|
91
|
+
end
|
92
|
+
|
93
|
+
def sub_template_message
|
94
|
+
if @sub_templates
|
95
|
+
"Trace of template inclusion: " +
|
96
|
+
@sub_templates.collect(&:inspect).join(", ")
|
97
|
+
else
|
98
|
+
""
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def source_extract(indentation = 0)
|
103
|
+
return [] unless (num = line_number)
|
104
|
+
num = num.to_i
|
105
|
+
|
106
|
+
source_code = @template.encode!.split("\n")
|
107
|
+
|
108
|
+
start_on_line = [num - SOURCE_CODE_RADIUS - 1, 0].max
|
109
|
+
end_on_line = [num + SOURCE_CODE_RADIUS - 1, source_code.length].min
|
110
|
+
|
111
|
+
indent = end_on_line.to_s.size + indentation
|
112
|
+
return [] unless (source_code = source_code[start_on_line..end_on_line])
|
113
|
+
|
114
|
+
formatted_code_for(source_code, start_on_line, indent)
|
115
|
+
end
|
116
|
+
|
117
|
+
def sub_template_of(template_path)
|
118
|
+
@sub_templates ||= []
|
119
|
+
@sub_templates << template_path
|
120
|
+
end
|
121
|
+
|
122
|
+
def line_number
|
123
|
+
@line_number ||=
|
124
|
+
if file_name
|
125
|
+
regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
|
126
|
+
$1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def source_location
|
133
|
+
if line_number
|
134
|
+
"on line ##{line_number} of "
|
135
|
+
else
|
136
|
+
"in "
|
137
|
+
end + file_name
|
138
|
+
end
|
139
|
+
|
140
|
+
def formatted_code_for(source_code, line_counter, indent)
|
141
|
+
raise("line counter is nil") if line_counter.nil?
|
142
|
+
indent_template = "%#{indent}s: %s"
|
143
|
+
source_code.map do |line|
|
144
|
+
line_counter += 1
|
145
|
+
indent_template % [line_counter, line]
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# {:id=>"", :location=>{:column=>13, :file=>"demo/e.html.svelte", :length=>3, :line=>7, :lineText=>"<h1>{hola}, {nam}</h1>", :namespace=>"file", :suggestion=>""}, :notes=>[], :pluginName=>"esbuild-svelte", :text=>"'nam' is not defined"}
|
data/lib/svelte/handler.rb
CHANGED
@@ -1,53 +1,65 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "svelte/helpers"
|
3
|
+
|
1
4
|
module Svelte
|
5
|
+
DISCARD_PROPS = %w[lookup_context view_renderer current_template output_buffer view_flow rendered_format marked_for_same_origin_verification virtual_path]
|
6
|
+
ISLAND_ATTRS = {
|
7
|
+
"on:visible": "",
|
8
|
+
"on:idle": ""
|
9
|
+
}
|
10
|
+
|
2
11
|
class Handler
|
3
|
-
include ActionView::Helpers::JavaScriptHelper
|
4
12
|
def call(template, source)
|
5
|
-
|
6
|
-
src = Tempfile.new
|
7
|
-
src.write source.b
|
8
|
-
src.rewind
|
9
|
-
|
10
|
-
file.write <<-JS
|
11
|
-
import { compile } from 'svelte/compiler';
|
13
|
+
digest = Digest::SHA256.base64digest(source)
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
+
<<-RUBY
|
16
|
+
template = ObjectSpace._id2ref(#{template.object_id})
|
17
|
+
|
18
|
+
Svelte.props.merge!(
|
19
|
+
instance_values.reject { |k,v|
|
20
|
+
k.start_with?("_") || Svelte::DISCARD_PROPS.include?(k)
|
21
|
+
}
|
22
|
+
) { |key,old,new| old }
|
23
|
+
|
24
|
+
Svelte.props.deep_symbolize_keys!
|
25
|
+
|
26
|
+
if local_assigns.dig(:svelte) == nil
|
27
|
+
local_assigns[:svelte] = {}
|
28
|
+
end
|
29
|
+
|
30
|
+
assembler = nil
|
31
|
+
|
32
|
+
assembler = Tempfile.new(['assembler', ".mjs"], '#{Svelte.gem_dir}/tmp')
|
33
|
+
|
34
|
+
assembler.write(ERB.new(File.read('#{Svelte.gem_dir}/lib/svelte/templates/assembler.js.erb')).result_with_hash({
|
35
|
+
path: '#{Rails.root.join template.identifier}',
|
36
|
+
locals: local_assigns,
|
37
|
+
compiled_client: j(Rails.cache.read('svelte/template/client' + '#{digest}')),
|
38
|
+
compiled_server: j(Rails.cache.read('svelte/template/server' + '#{digest}')),
|
39
|
+
ssr: Svelte.precedence(local_assigns.dig(:svelte, :ssr), template.variant.nil? ? nil : (template.variant == "server"), Svelte.ssr)
|
40
|
+
}))
|
41
|
+
|
42
|
+
assembler.rewind
|
15
43
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
44
|
+
result = JSON.parse(`NODE_NO_WARNINGS=1 node --experimental-vm-modules \#{assembler.path}`).deep_symbolize_keys
|
45
|
+
|
46
|
+
if result[:error]
|
47
|
+
e = Svelte::CompilerError.new(result.dig(:error, :text), result.dig(:error, :location))
|
48
|
+
raise Svelte::TemplateError.new(template, e)
|
49
|
+
end
|
50
|
+
|
51
|
+
assembler&.close
|
52
|
+
assembler&.unlink
|
53
|
+
|
54
|
+
Rails.cache.write("svelte/template/client/" + '#{digest}', result[:compiled][:client], expires_in: 14.days)
|
55
|
+
Rails.cache.write("svelte/template/server/" + '#{digest}', result[:compiled][:server], expires_in: 14.days)
|
56
|
+
|
57
|
+
ERB.new(File.read("#{Svelte.gem_dir}/lib/svelte/templates/island.html.erb")).result_with_hash({ result:, locals: local_assigns, digest: "#{digest}"})
|
25
58
|
RUBY
|
26
59
|
end
|
27
60
|
|
28
61
|
def self.call(template, source)
|
29
62
|
new.call(template, source)
|
30
63
|
end
|
31
|
-
|
32
|
-
def self.generate_island(js, props)
|
33
|
-
return <<-HTML
|
34
|
-
<is-land on:visible on:idle>
|
35
|
-
<template data-island>
|
36
|
-
<div id="svelte-app-client">
|
37
|
-
</div>
|
38
|
-
|
39
|
-
<script type="module">
|
40
|
-
#{js}
|
41
|
-
|
42
|
-
new Component({
|
43
|
-
target: document.getElementById("svelte-app-client"),
|
44
|
-
props: JSON.parse(#{props.to_json.presence || "{}"})
|
45
|
-
});
|
46
|
-
</script>
|
47
|
-
|
48
|
-
</template>
|
49
|
-
</is-land>
|
50
|
-
HTML
|
51
|
-
end
|
52
64
|
end
|
53
|
-
end
|
65
|
+
end
|
data/lib/svelte/helpers.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
module Svelte
|
2
|
-
module Helpers
|
2
|
+
module Helpers
|
3
3
|
include ActionView::Helpers::JavaScriptHelper
|
4
4
|
def svelte_tags
|
5
5
|
js = <<-JS
|
@@ -9,8 +9,18 @@ module Svelte
|
|
9
9
|
|
10
10
|
window.props = readable(JSON.parse("#{j(Svelte.props.to_json)}"))
|
11
11
|
JS
|
12
|
-
|
12
|
+
|
13
13
|
tag.script(js.html_safe, type: "module")
|
14
14
|
end
|
15
|
+
|
16
|
+
delegate :destructure_attrs, to: :class
|
17
|
+
|
18
|
+
def self.destructure_attrs(hash)
|
19
|
+
string = ""
|
20
|
+
hash.each do |k, v|
|
21
|
+
string += "#{k}='#{j v}' "
|
22
|
+
end
|
23
|
+
string
|
24
|
+
end
|
15
25
|
end
|
16
|
-
end
|
26
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
/*
|
2
|
+
IF YOU ARE READING THIS, YOU ARE VIEWING THE BUILT JS FROM THE SOURCE TYPESCRIPT AT ../ts/ .
|
3
|
+
|
4
|
+
DO NOT MODIFY.
|
5
|
+
*/
|
6
|
+
|
7
|
+
import { readable } from "svelte/store";
|
8
|
+
import { importFromStringSync } from "module-from-string";
|
9
|
+
import * as esbuild from "esbuild";
|
10
|
+
import sveltePlugin from "esbuild-svelte";
|
11
|
+
import { sveltePreprocess } from "svelte-preprocess";
|
12
|
+
import * as recast from "recast";
|
13
|
+
class Builder {
|
14
|
+
path;
|
15
|
+
props;
|
16
|
+
locals;
|
17
|
+
compiled;
|
18
|
+
ssr;
|
19
|
+
workingDir;
|
20
|
+
preprocess;
|
21
|
+
constructor(path, props, locals, client, server, ssr, workingDir, preprocess) {
|
22
|
+
this.path = path;
|
23
|
+
this.props = readable(props);
|
24
|
+
this.locals = locals;
|
25
|
+
this.compiled = {
|
26
|
+
client,
|
27
|
+
server
|
28
|
+
};
|
29
|
+
this.ssr = ssr;
|
30
|
+
this.workingDir = workingDir;
|
31
|
+
this.preprocess = preprocess;
|
32
|
+
}
|
33
|
+
async bundle(generate, sveltePath = "svelte") {
|
34
|
+
const bundle = await esbuild.build({
|
35
|
+
entryPoints: [this.path],
|
36
|
+
mainFields: ["svelte", "browser", "module", "main"],
|
37
|
+
conditions: ["svelte", "browser"],
|
38
|
+
absWorkingDir: this.workingDir,
|
39
|
+
write: false,
|
40
|
+
outfile: "component.js",
|
41
|
+
bundle: true,
|
42
|
+
format: "esm",
|
43
|
+
metafile: true,
|
44
|
+
plugins: [
|
45
|
+
// @ts-expect-error
|
46
|
+
sveltePlugin({
|
47
|
+
compilerOptions: {
|
48
|
+
generate,
|
49
|
+
css: "injected",
|
50
|
+
hydratable: true,
|
51
|
+
sveltePath
|
52
|
+
},
|
53
|
+
preprocess: sveltePreprocess(this.preprocess),
|
54
|
+
filterWarnings: (warning) => {
|
55
|
+
if (warning.code === "missing-declaration" && warning.message.includes("'props'")) {
|
56
|
+
return false;
|
57
|
+
}
|
58
|
+
return true;
|
59
|
+
}
|
60
|
+
})
|
61
|
+
]
|
62
|
+
});
|
63
|
+
const throwables = [].concat(bundle.errors, bundle.warnings);
|
64
|
+
if (throwables.length > 0) {
|
65
|
+
throw throwables[0];
|
66
|
+
}
|
67
|
+
return bundle.outputFiles[0].text;
|
68
|
+
}
|
69
|
+
async client() {
|
70
|
+
return this.compiled?.client || this.standardizeClient(await this.bundle("dom", "https://esm.sh/svelte"));
|
71
|
+
}
|
72
|
+
standardizeClient(code) {
|
73
|
+
const ast = recast.parse(code);
|
74
|
+
let name;
|
75
|
+
recast.visit(ast, {
|
76
|
+
visitExportNamedDeclaration: (path) => {
|
77
|
+
const stagingName = path.node?.specifiers?.[0].local?.name;
|
78
|
+
name = typeof stagingName !== "string" ? "" : stagingName;
|
79
|
+
path.prune();
|
80
|
+
return false;
|
81
|
+
}
|
82
|
+
});
|
83
|
+
recast.visit(ast, {
|
84
|
+
visitIdentifier: (path) => {
|
85
|
+
if (path.node.name === name) {
|
86
|
+
path.node.name = "App";
|
87
|
+
}
|
88
|
+
return false;
|
89
|
+
}
|
90
|
+
});
|
91
|
+
return recast.print(ast).code;
|
92
|
+
}
|
93
|
+
async server() {
|
94
|
+
const output = this.compiled?.server || await this.bundle("ssr");
|
95
|
+
const Component = importFromStringSync(output, {
|
96
|
+
globals: { props: this.props }
|
97
|
+
}).default;
|
98
|
+
const { html, css } = await Component.render(this.locals);
|
99
|
+
return { output, html, css: css.code };
|
100
|
+
}
|
101
|
+
async build() {
|
102
|
+
try {
|
103
|
+
const serv = this.ssr ? await this.server() : null;
|
104
|
+
const cli = await this.client();
|
105
|
+
const comp = {
|
106
|
+
client: cli,
|
107
|
+
server: serv?.output
|
108
|
+
};
|
109
|
+
return {
|
110
|
+
client: cli,
|
111
|
+
server: serv,
|
112
|
+
compiled: comp
|
113
|
+
};
|
114
|
+
} catch (e) {
|
115
|
+
return { error: e };
|
116
|
+
}
|
117
|
+
}
|
118
|
+
}
|
119
|
+
var builder_default = Builder;
|
120
|
+
export {
|
121
|
+
builder_default as default
|
122
|
+
};
|
data/lib/svelte/railtie.rb
CHANGED
@@ -3,11 +3,15 @@ require "svelte/helpers"
|
|
3
3
|
|
4
4
|
module Svelte
|
5
5
|
class Railtie < ::Rails::Railtie
|
6
|
-
initializer "svelte
|
6
|
+
initializer "svelte" do |app|
|
7
7
|
ActiveSupport.on_load :action_view do
|
8
8
|
ActionView::Template.register_template_handler :svelte, Svelte::Handler
|
9
9
|
ActionView::Base.send :include, Svelte::Helpers
|
10
10
|
end
|
11
|
+
|
12
|
+
ActionController::Base.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
13
|
+
include Svelte::Variabilization
|
14
|
+
RUBY
|
11
15
|
end
|
12
16
|
end
|
13
17
|
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
import Builder from "<%= "#{Svelte.gem_dir}/lib/svelte/js/builder.js" %>";
|
2
|
+
|
3
|
+
const bob = new Builder(
|
4
|
+
'<%= path %>',
|
5
|
+
JSON.parse('<%= Svelte.props.to_json || "{}" %>'),
|
6
|
+
JSON.parse('<%= locals.to_json || "{}" %>'),
|
7
|
+
'<%= compiled_client || "" %>',
|
8
|
+
'<%= compiled_server || "" %>',
|
9
|
+
<%= ssr %>,
|
10
|
+
'<%= Rails.root %>',
|
11
|
+
JSON.parse('<%= Svelte.preprocess.to_json || "{}" %>')
|
12
|
+
)
|
13
|
+
|
14
|
+
const built = await bob.build()
|
15
|
+
|
16
|
+
process.stdout.write(JSON.stringify(built))
|
@@ -0,0 +1,20 @@
|
|
1
|
+
<is-land <%= Svelte::Helpers.destructure_attrs((locals.dig(:svelte, :island) rescue nil) || Svelte::ISLAND_ATTRS) %>>
|
2
|
+
<div id="hydratee-<%= digest %>">
|
3
|
+
<%= result.dig(:server, :html) %>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<style>
|
7
|
+
<%= result.dig(:server, :css) %>
|
8
|
+
</style>
|
9
|
+
<template data-island>
|
10
|
+
<script type="module">
|
11
|
+
<%= result.dig(:client) %>
|
12
|
+
|
13
|
+
new App({
|
14
|
+
target: document.getElementById("hydratee-<%= digest %>"),
|
15
|
+
props: JSON.parse('<%= locals.to_json %>'),
|
16
|
+
hydrate: true
|
17
|
+
});
|
18
|
+
</script>
|
19
|
+
</template>
|
20
|
+
</is-land>
|
data/lib/svelte/version.rb
CHANGED
data/lib/svelte.rb
CHANGED
@@ -1,11 +1,40 @@
|
|
1
|
+
require "active_support/core_ext"
|
2
|
+
require "svelte/variabilization"
|
1
3
|
require "svelte/railtie"
|
2
|
-
require "svelte/
|
4
|
+
require "svelte/errors"
|
3
5
|
require "svelte/helpers"
|
6
|
+
require "svelte/handler"
|
4
7
|
require "svelte/version"
|
5
8
|
require "active_support/isolated_execution_state"
|
6
9
|
|
7
10
|
module Svelte
|
11
|
+
include ActionView::Helpers::JavaScriptHelper
|
12
|
+
|
13
|
+
def self.configure
|
14
|
+
yield(self)
|
15
|
+
end
|
16
|
+
|
17
|
+
# @dynamic self.ssr, self.aliases, self.preprocess
|
18
|
+
# @dynamic self.ssr=, self.aliases=, self.preprocess=
|
19
|
+
mattr_accessor :ssr, default: true
|
20
|
+
mattr_accessor :aliases, default: {}
|
21
|
+
mattr_accessor :preprocess, default: {}
|
22
|
+
|
8
23
|
def self.props
|
9
|
-
ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {}
|
24
|
+
ActiveSupport::IsolatedExecutionState[:svelte_props] ||= {} # steep:ignore UnknownConstant
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.gem_dir
|
28
|
+
Gem::Specification.find_by_name("actionview-svelte-handler").gem_dir
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.precedence(*args)
|
32
|
+
args.each do |v|
|
33
|
+
if !v.nil?
|
34
|
+
return v
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
nil
|
10
39
|
end
|
11
40
|
end
|