klenod-build 0.0.22 → 0.0.24
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/lib/klenod/build/plugins/haml_plugin/parser.rb +49 -4
- data/lib/klenod/build/plugins/haml_plugin/transformer/ruby_builder.rb +6 -6
- data/lib/klenod/build/plugins/haml_plugin/transformer.rb +9 -1
- data/lib/klenod/build/plugins/markdown_compiler.rb +73 -94
- data/lib/klenod/build/plugins/text_interpolation.rb +153 -0
- data/lib/klenod/build/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6f22c9acfae008123fd3b68c5ab499c7bc8df3da4d5e135dea331f0fe6563596
|
|
4
|
+
data.tar.gz: da87ac500d4d0f26bdeb4ff3249afacd1e34d594bb2f4e18bff1c1c4dd818e64
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0dd4bce3f8c250ed256e4362e6337f8ddee38c2865b9aba9ae7153066b1fb4d34f0fb324714626b4787011fde4118b1bc3f80a487865e2ee409248aebb44375f
|
|
7
|
+
data.tar.gz: a65805c594f8d0823ce2df36a1b9d2f5c00e7b5ff71d3a178c1b8d7cc5f24a0f69d09d5d254d8bd16479d9c476b3ac14f67d097ea28efb2ccc7d89fd34f6dcec
|
|
@@ -40,13 +40,28 @@ module Klenod
|
|
|
40
40
|
# `video` plus a `.id` class. Extend that form to ordinary Ruby
|
|
41
41
|
# member and index expressions while leaving all other attributes on
|
|
42
42
|
# Haml's native parsing path.
|
|
43
|
+
#
|
|
44
|
+
# Haml also continues a parenthesized list onto following lines only
|
|
45
|
+
# after the first attribute, so `%a(` followed by a line break is an
|
|
46
|
+
# invalid attribute list. Join the lines of an unclosed list first so
|
|
47
|
+
# both forms work across lines.
|
|
43
48
|
def parse_new_attributes(text)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
joined_lines = 0
|
|
50
|
+
until (close_index = attribute_list_close_index(text)) || @next_line.eod?
|
|
51
|
+
text = "#{text} #{@next_line.text}"
|
|
52
|
+
joined_lines += 1
|
|
53
|
+
next_line
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
pairs = close_index && extended_attribute_pairs(text[1...close_index])
|
|
57
|
+
rest = close_index && text[(close_index + 1)..]
|
|
58
|
+
unless pairs&.any? { |_name, value| value.match?(/[.\[]/) }
|
|
59
|
+
attributes, rest, last_line = super
|
|
60
|
+
return [attributes, rest, last_line + joined_lines]
|
|
61
|
+
end
|
|
47
62
|
|
|
48
63
|
dynamic = pairs.reduce("{") { |source, (name, value)| "#{source}#{::Haml::Util.inspect_obj(name)} => #{value}," } << "}"
|
|
49
|
-
[[{}, dynamic], rest, @line.index + 1]
|
|
64
|
+
[[{}, dynamic], rest, @line.index + 1 + joined_lines]
|
|
50
65
|
end
|
|
51
66
|
|
|
52
67
|
def annotate_tag_nodes(root)
|
|
@@ -132,6 +147,36 @@ module Klenod
|
|
|
132
147
|
pairs
|
|
133
148
|
end
|
|
134
149
|
|
|
150
|
+
# Returns the index of the `)` that closes the list opened at index 0,
|
|
151
|
+
# ignoring brackets inside quoted values.
|
|
152
|
+
def attribute_list_close_index(source)
|
|
153
|
+
depth = 0
|
|
154
|
+
quote = nil
|
|
155
|
+
escaped = false
|
|
156
|
+
|
|
157
|
+
source.each_char.with_index do |character, index|
|
|
158
|
+
if quote
|
|
159
|
+
if escaped
|
|
160
|
+
escaped = false
|
|
161
|
+
elsif character == "\\"
|
|
162
|
+
escaped = true
|
|
163
|
+
elsif character == quote
|
|
164
|
+
quote = nil
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
case character
|
|
168
|
+
when "'", '"' then quote = character
|
|
169
|
+
when "(", "[", "{" then depth += 1
|
|
170
|
+
when ")", "]", "}"
|
|
171
|
+
depth -= 1
|
|
172
|
+
return index if depth.zero?
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
nil
|
|
178
|
+
end
|
|
179
|
+
|
|
135
180
|
def attribute_boundary?(remaining_source)
|
|
136
181
|
attribute_sequence?(remaining_source)
|
|
137
182
|
end
|
|
@@ -585,7 +585,7 @@ module Klenod
|
|
|
585
585
|
return source unless source.include?("__LINE__")
|
|
586
586
|
|
|
587
587
|
line_offsets = [0]
|
|
588
|
-
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.
|
|
588
|
+
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.bytesize }
|
|
589
589
|
|
|
590
590
|
Ripper
|
|
591
591
|
.lex(source)
|
|
@@ -593,7 +593,7 @@ module Klenod
|
|
|
593
593
|
.reverse_each
|
|
594
594
|
.each_with_object(source.dup) do |((line, column), _type, token, _state), rewritten|
|
|
595
595
|
offset = line_offsets.fetch(line - 1) + column
|
|
596
|
-
rewritten
|
|
596
|
+
rewritten.bytesplice(offset, token.bytesize, line_no.to_s)
|
|
597
597
|
end
|
|
598
598
|
end
|
|
599
599
|
|
|
@@ -607,7 +607,7 @@ module Klenod
|
|
|
607
607
|
return source if @variables.empty?
|
|
608
608
|
|
|
609
609
|
line_offsets = [0]
|
|
610
|
-
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.
|
|
610
|
+
source.each_line(chomp: false) { |line| line_offsets << line_offsets.last + line.bytesize }
|
|
611
611
|
|
|
612
612
|
tokens = Ripper.lex(source)
|
|
613
613
|
|
|
@@ -617,17 +617,17 @@ module Klenod
|
|
|
617
617
|
kind, prefix, pattern = VARIABLE_TOKEN_KINDS[type]
|
|
618
618
|
receiver = @variables[kind]
|
|
619
619
|
if type == :on_gvar && token == "$*" && receiver
|
|
620
|
-
[offset_for(line_offsets, line, column), token.
|
|
620
|
+
[offset_for(line_offsets, line, column), token.bytesize, receiver]
|
|
621
621
|
elsif receiver && token.match?(pattern)
|
|
622
622
|
name = token.delete_prefix(prefix)
|
|
623
623
|
replacement = "(#{receiver})[#{symbol_source(name)}]"
|
|
624
624
|
replacement = "{#{replacement}}" if tokens[index - 1]&.fetch(1) == :on_embvar
|
|
625
|
-
[offset_for(line_offsets, line, column), token.
|
|
625
|
+
[offset_for(line_offsets, line, column), token.bytesize, replacement]
|
|
626
626
|
end
|
|
627
627
|
end
|
|
628
628
|
.reverse_each
|
|
629
629
|
.each_with_object(source.dup) do |(offset, length, replacement), rewritten|
|
|
630
|
-
rewritten
|
|
630
|
+
rewritten.bytesplice(offset, length, replacement)
|
|
631
631
|
end
|
|
632
632
|
end
|
|
633
633
|
|
|
@@ -5,6 +5,7 @@ require "ripper"
|
|
|
5
5
|
|
|
6
6
|
require "klenod/runtime/source_map"
|
|
7
7
|
require_relative "../markdown_compiler"
|
|
8
|
+
require_relative "../text_interpolation"
|
|
8
9
|
require_relative "parser"
|
|
9
10
|
|
|
10
11
|
module Klenod
|
|
@@ -613,11 +614,18 @@ module Klenod
|
|
|
613
614
|
def compile_filter_node(node, builder:, markdown_compiler:)
|
|
614
615
|
return builder.render_ruby_filter(compile_ruby_filter(node, builder: builder)) if ruby_filter?(node)
|
|
615
616
|
return builder.expression(markdown_compiler.compile(node.value.fetch(:text), interpolate: true)) if markdown_filter?(node)
|
|
616
|
-
return
|
|
617
|
+
return compile_plain_filter(node.value.fetch(:text).to_s, builder: builder) if plain_filter?(node)
|
|
617
618
|
|
|
618
619
|
raise ArgumentError, "Only :ruby, :markdown, and :plain Haml filters are supported"
|
|
619
620
|
end
|
|
620
621
|
|
|
622
|
+
def compile_plain_filter(text, builder:)
|
|
623
|
+
source = TextInterpolation::Source.new(text)
|
|
624
|
+
return builder.literal(source.literal(source.text)) unless source.interpolated?
|
|
625
|
+
|
|
626
|
+
builder.expression("[#{source.expressions(source.text).join(", ")}]")
|
|
627
|
+
end
|
|
628
|
+
|
|
621
629
|
def ruby_filter?(node)
|
|
622
630
|
node.type == :filter && node.value.fetch(:name) == "ruby"
|
|
623
631
|
end
|
|
@@ -9,11 +9,13 @@ ensure
|
|
|
9
9
|
$VERBOSE = previous_verbose
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
require_relative "text_interpolation"
|
|
13
|
+
|
|
12
14
|
module Klenod
|
|
13
15
|
module Build
|
|
14
16
|
module Plugins
|
|
15
17
|
class MarkdownCompiler
|
|
16
|
-
|
|
18
|
+
TYPOGRAPHIC_TEXT = Kramdown::Converter::Html::TYPOGRAPHIC_SYMS.transform_values { |entities| entities.map(&:char).join }.freeze
|
|
17
19
|
|
|
18
20
|
def initialize(factory:, components_source: "{}")
|
|
19
21
|
@factory = factory
|
|
@@ -21,69 +23,95 @@ module Klenod
|
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def compile(source, interpolate: false)
|
|
24
|
-
|
|
25
|
-
document = Kramdown::Document.new(source, input: "GFM", hard_wrap: false)
|
|
26
|
-
compile_children(document.root.children
|
|
26
|
+
@interpolation = TextInterpolation::Source.new(source) if interpolate
|
|
27
|
+
document = Kramdown::Document.new(@interpolation&.text || source, input: "GFM", hard_wrap: false)
|
|
28
|
+
compile_children(document.root.children)
|
|
29
|
+
ensure
|
|
30
|
+
@interpolation = nil
|
|
27
31
|
end
|
|
28
32
|
|
|
29
33
|
private
|
|
30
34
|
|
|
31
|
-
def compile_children(children, parent: nil
|
|
32
|
-
expressions = compile_child_expressions(children, parent: parent
|
|
35
|
+
def compile_children(children, parent: nil)
|
|
36
|
+
expressions = compile_child_expressions(children, parent: parent)
|
|
33
37
|
return "nil" if expressions.empty?
|
|
34
38
|
return expressions.fetch(0) if expressions.length == 1
|
|
35
39
|
|
|
36
40
|
"[#{expressions.join(", ")}]"
|
|
37
41
|
end
|
|
38
42
|
|
|
39
|
-
def compile_child_expressions(children, parent:,
|
|
40
|
-
children.flat_map { |child| compile_node(child, parent: parent, table_section: table_section
|
|
43
|
+
def compile_child_expressions(children, parent:, table_section: nil)
|
|
44
|
+
merge_text_nodes(children).flat_map { |child| compile_node(child, parent: parent, table_section: table_section) }.compact
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Kramdown splits typographic replacements and entities out of text.
|
|
48
|
+
# Fold them back into neighboring text so "it's" stays one string.
|
|
49
|
+
def merge_text_nodes(children)
|
|
50
|
+
children.each_with_object([]) do |child, merged|
|
|
51
|
+
text = inline_text(child)
|
|
52
|
+
if text.nil?
|
|
53
|
+
merged << child
|
|
54
|
+
elsif merged.last&.type == :text
|
|
55
|
+
merged[-1] = Kramdown::Element.new(:text, merged.last.value + text)
|
|
56
|
+
else
|
|
57
|
+
merged << Kramdown::Element.new(:text, text)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def inline_text(node)
|
|
63
|
+
case node.type
|
|
64
|
+
when :text then node.value
|
|
65
|
+
when :smart_quote then Kramdown::Utils::Entities.entity(node.value.to_s).char
|
|
66
|
+
when :typographic_sym then TYPOGRAPHIC_TEXT.fetch(node.value)
|
|
67
|
+
when :entity then node.value.char
|
|
68
|
+
end
|
|
41
69
|
end
|
|
42
70
|
|
|
43
|
-
def compile_node(node, parent: nil, table_section: nil
|
|
71
|
+
def compile_node(node, parent: nil, table_section: nil)
|
|
44
72
|
case node.type
|
|
45
73
|
when :root
|
|
46
|
-
[compile_children(node.children, parent: node
|
|
74
|
+
[compile_children(node.children, parent: node)]
|
|
47
75
|
when :blank
|
|
48
76
|
nil
|
|
49
77
|
when :text
|
|
50
|
-
text_expressions(node.value
|
|
78
|
+
text_expressions(node.value)
|
|
51
79
|
when :p
|
|
52
|
-
return compile_child_expressions(node.children, parent: node
|
|
80
|
+
return compile_child_expressions(node.children, parent: node) if node.options[:transparent]
|
|
53
81
|
|
|
54
|
-
[factory_call(:p, node.children, attrs: node.attr, parent: node
|
|
82
|
+
[factory_call(:p, node.children, attrs: node.attr, parent: node)]
|
|
55
83
|
when :header
|
|
56
|
-
[factory_call(:"h#{node.options.fetch(:level)}", node.children, attrs: node
|
|
84
|
+
[factory_call(:"h#{node.options.fetch(:level)}", node.children, attrs: header_attrs(node), parent: node)]
|
|
57
85
|
when :em, :strong, :a, :blockquote, :ul, :ol, :li, :table
|
|
58
|
-
[factory_call(node.type, node.children, attrs: node.attr, parent: node
|
|
86
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node)]
|
|
59
87
|
when :thead, :tbody
|
|
60
|
-
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: node.type
|
|
88
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: node.type)]
|
|
61
89
|
when :tr
|
|
62
|
-
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: table_section
|
|
90
|
+
[factory_call(node.type, node.children, attrs: node.attr, parent: node, table_section: table_section)]
|
|
63
91
|
when :td
|
|
64
|
-
[factory_call((table_section == :thead) ? :th : :td, node.children, attrs: node.attr, parent: node, table_section: table_section
|
|
92
|
+
[factory_call((table_section == :thead) ? :th : :td, node.children, attrs: node.attr, parent: node, table_section: table_section)]
|
|
65
93
|
when :img, :hr, :br
|
|
66
|
-
[factory_call(node.type, [], attrs: node.attr, parent: node
|
|
94
|
+
[factory_call(node.type, [], attrs: node.attr, parent: node)]
|
|
67
95
|
when :codespan
|
|
68
|
-
[factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node
|
|
96
|
+
[factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node)]
|
|
69
97
|
when :codeblock
|
|
70
|
-
code = factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node
|
|
71
|
-
[factory_call(:pre, [], child_sources: [code], parent: node
|
|
98
|
+
code = factory_call(:code, [node], raw_text: node.value, attrs: node.attr, parent: node)
|
|
99
|
+
[factory_call(:pre, [], child_sources: [code], parent: node)]
|
|
72
100
|
when :html_element
|
|
73
|
-
[factory_call(node.value.to_sym, node.children, attrs: node.attr, parent: node
|
|
101
|
+
[factory_call(node.value.to_sym, node.children, attrs: node.attr, parent: node)]
|
|
74
102
|
when :raw
|
|
75
|
-
[
|
|
103
|
+
[literal_text(node.value.to_s).inspect]
|
|
76
104
|
else
|
|
77
|
-
[compile_children(node.children, parent: node
|
|
105
|
+
[compile_children(node.children, parent: node)]
|
|
78
106
|
end
|
|
79
107
|
end
|
|
80
108
|
|
|
81
|
-
def factory_call(tag, children, parent:, attrs: {}, raw_text: nil, child_sources: nil, table_section: nil
|
|
109
|
+
def factory_call(tag, children, parent:, attrs: {}, raw_text: nil, child_sources: nil, table_section: nil)
|
|
82
110
|
child_sources ||=
|
|
83
111
|
if raw_text.nil?
|
|
84
|
-
compile_child_expressions(children, parent: parent, table_section: table_section
|
|
112
|
+
compile_child_expressions(children, parent: parent, table_section: table_section)
|
|
85
113
|
else
|
|
86
|
-
[
|
|
114
|
+
[literal_text(raw_text).inspect]
|
|
87
115
|
end
|
|
88
116
|
tag_source = tag_source(tag)
|
|
89
117
|
parts = [tag_source, *child_sources]
|
|
@@ -97,82 +125,33 @@ module Klenod
|
|
|
97
125
|
end
|
|
98
126
|
|
|
99
127
|
def attrs_source(attrs)
|
|
100
|
-
attrs.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def text_expressions(text, interpolate:)
|
|
104
|
-
return [restore_escaped_interpolation(text).inspect] unless interpolate && text.include?("\#{")
|
|
105
|
-
|
|
106
|
-
expressions = []
|
|
107
|
-
buffer = +""
|
|
108
|
-
index = 0
|
|
109
|
-
|
|
110
|
-
while index < text.length
|
|
111
|
-
if text[index, 2] == "\#{"
|
|
112
|
-
expression, next_index = read_interpolation(text, index + 2)
|
|
113
|
-
if expression
|
|
114
|
-
expressions << restore_escaped_interpolation(buffer).inspect unless buffer.empty?
|
|
115
|
-
buffer.clear
|
|
116
|
-
expressions << "(#{expression})"
|
|
117
|
-
index = next_index
|
|
118
|
-
else
|
|
119
|
-
buffer << text[index]
|
|
120
|
-
index += 1
|
|
121
|
-
end
|
|
122
|
-
else
|
|
123
|
-
buffer << text[index]
|
|
124
|
-
index += 1
|
|
125
|
-
end
|
|
128
|
+
pairs = attrs.map do |name, value|
|
|
129
|
+
value_source = value.is_a?(String) ? string_source(value) : value.inspect
|
|
130
|
+
"#{name.to_sym.inspect} => #{value_source}"
|
|
126
131
|
end
|
|
127
132
|
|
|
128
|
-
|
|
129
|
-
expressions
|
|
133
|
+
"{#{pairs.join(", ")}}"
|
|
130
134
|
end
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
while index < text.length
|
|
139
|
-
char = text[index]
|
|
140
|
-
|
|
141
|
-
if quote
|
|
142
|
-
expression << char
|
|
143
|
-
if escaped
|
|
144
|
-
escaped = false
|
|
145
|
-
elsif char == "\\"
|
|
146
|
-
escaped = true
|
|
147
|
-
elsif char == quote
|
|
148
|
-
quote = nil
|
|
149
|
-
end
|
|
150
|
-
else
|
|
151
|
-
case char
|
|
152
|
-
when "\"", "'", "`"
|
|
153
|
-
quote = char
|
|
154
|
-
when "{"
|
|
155
|
-
depth += 1
|
|
156
|
-
when "}"
|
|
157
|
-
depth -= 1
|
|
158
|
-
return [expression.strip, index + 1] if depth.zero?
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
expression << char
|
|
162
|
-
end
|
|
136
|
+
# Kramdown derives heading IDs from text that still contains
|
|
137
|
+
# interpolation tokens, which leaves stray separators behind.
|
|
138
|
+
def header_attrs(node)
|
|
139
|
+
id = node.attr["id"]
|
|
140
|
+
return node.attr unless id && @interpolation&.interpolated?(node.options[:raw_text].to_s)
|
|
163
141
|
|
|
164
|
-
|
|
165
|
-
|
|
142
|
+
node.attr.merge("id" => id.gsub(/-{2,}/, "-").delete_prefix("-").delete_suffix("-"))
|
|
143
|
+
end
|
|
166
144
|
|
|
167
|
-
|
|
145
|
+
def text_expressions(text)
|
|
146
|
+
@interpolation ? @interpolation.expressions(text) : [text.inspect]
|
|
168
147
|
end
|
|
169
148
|
|
|
170
|
-
def
|
|
171
|
-
|
|
149
|
+
def string_source(text)
|
|
150
|
+
@interpolation ? @interpolation.string_source(text) : text.inspect
|
|
172
151
|
end
|
|
173
152
|
|
|
174
|
-
def
|
|
175
|
-
|
|
153
|
+
def literal_text(text)
|
|
154
|
+
@interpolation ? @interpolation.literal(text) : text
|
|
176
155
|
end
|
|
177
156
|
end
|
|
178
157
|
end
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Klenod
|
|
4
|
+
module Build
|
|
5
|
+
module Plugins
|
|
6
|
+
# Compiles Ruby-style `#{...}` interpolation in filter text into Ruby
|
|
7
|
+
# source. `\#{` stays literal.
|
|
8
|
+
#
|
|
9
|
+
# Each interpolation is replaced by an opaque private-use token before
|
|
10
|
+
# the text is parsed further, so parsers such as Markdown never see the
|
|
11
|
+
# Ruby expression. Compile the parsed pieces back with the same Source.
|
|
12
|
+
module TextInterpolation
|
|
13
|
+
TOKEN_START = ""
|
|
14
|
+
TOKEN_END = ""
|
|
15
|
+
ESCAPE_TOKEN = ""
|
|
16
|
+
FIRST_INDEX_CODEPOINT = 0xE100
|
|
17
|
+
LAST_INDEX_CODEPOINT = 0xF8FF
|
|
18
|
+
TOKEN_PATTERN = /#{TOKEN_START}([\u{E100}-\u{F8FF}])#{TOKEN_END}/o
|
|
19
|
+
SEGMENT_PATTERN = /#{TOKEN_START}[\u{E100}-\u{F8FF}]#{TOKEN_END}|#{ESCAPE_TOKEN}/o
|
|
20
|
+
|
|
21
|
+
class Source
|
|
22
|
+
# The source with interpolations and escapes replaced by tokens.
|
|
23
|
+
attr_reader :text
|
|
24
|
+
|
|
25
|
+
def initialize(source)
|
|
26
|
+
@interpolations = []
|
|
27
|
+
@text = tokenize(source)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def interpolated?(text = @text)
|
|
31
|
+
text.match?(TOKEN_PATTERN)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Returns Ruby source for each literal and interpolated segment of
|
|
35
|
+
# tokenized text.
|
|
36
|
+
def expressions(text)
|
|
37
|
+
segments(text).map { |kind, value| (kind == :ruby) ? "(#{value})" : value.inspect }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns Ruby source for one String built from tokenized text.
|
|
41
|
+
def string_source(text)
|
|
42
|
+
parts = segments(text)
|
|
43
|
+
return literal(text).inspect if parts.none? { |kind, _| kind == :ruby }
|
|
44
|
+
|
|
45
|
+
body = parts.map { |kind, value| (kind == :ruby) ? "\#{#{value}}" : value.inspect[1...-1] }.join
|
|
46
|
+
"\"#{body}\""
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Restores tokenized text to its original characters, keeping
|
|
50
|
+
# interpolations as literal `#{...}` text.
|
|
51
|
+
def literal(text)
|
|
52
|
+
text.gsub(SEGMENT_PATTERN) do |token|
|
|
53
|
+
(token == ESCAPE_TOKEN) ? "\#{" : @interpolations.fetch(token_index(token)).fetch(:raw)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def segments(text)
|
|
60
|
+
segments = []
|
|
61
|
+
buffer = +""
|
|
62
|
+
|
|
63
|
+
text.split(/(#{SEGMENT_PATTERN})/o).each do |part|
|
|
64
|
+
if part == ESCAPE_TOKEN
|
|
65
|
+
buffer << "\#{"
|
|
66
|
+
elsif part.match?(TOKEN_PATTERN)
|
|
67
|
+
segments << [:text, buffer] unless buffer.empty?
|
|
68
|
+
buffer = +""
|
|
69
|
+
segments << [:ruby, @interpolations.fetch(token_index(part)).fetch(:expression)]
|
|
70
|
+
else
|
|
71
|
+
buffer << part
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
segments << [:text, buffer] unless buffer.empty?
|
|
76
|
+
segments
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def token_index(token)
|
|
80
|
+
token[TOKEN_PATTERN, 1].ord - FIRST_INDEX_CODEPOINT
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def tokenize(source)
|
|
84
|
+
output = +""
|
|
85
|
+
index = 0
|
|
86
|
+
|
|
87
|
+
while index < source.length
|
|
88
|
+
if source[index, 3] == "\\\#{"
|
|
89
|
+
output << ESCAPE_TOKEN
|
|
90
|
+
index += 3
|
|
91
|
+
elsif source[index, 2] == "\#{" && (interpolation = read_interpolation(source, index + 2))
|
|
92
|
+
expression, next_index = interpolation
|
|
93
|
+
output << token(expression, raw: source[index...next_index])
|
|
94
|
+
index = next_index
|
|
95
|
+
else
|
|
96
|
+
output << source[index]
|
|
97
|
+
index += 1
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
output
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def token(expression, raw:)
|
|
105
|
+
codepoint = FIRST_INDEX_CODEPOINT + @interpolations.length
|
|
106
|
+
raise ArgumentError, "Too many interpolations in one filter" if codepoint > LAST_INDEX_CODEPOINT
|
|
107
|
+
|
|
108
|
+
@interpolations << {expression: expression, raw: raw}
|
|
109
|
+
"#{TOKEN_START}#{codepoint.chr(Encoding::UTF_8)}#{TOKEN_END}"
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def read_interpolation(text, index)
|
|
113
|
+
expression = +""
|
|
114
|
+
depth = 1
|
|
115
|
+
quote = nil
|
|
116
|
+
escaped = false
|
|
117
|
+
|
|
118
|
+
while index < text.length
|
|
119
|
+
char = text[index]
|
|
120
|
+
|
|
121
|
+
if quote
|
|
122
|
+
expression << char
|
|
123
|
+
if escaped
|
|
124
|
+
escaped = false
|
|
125
|
+
elsif char == "\\"
|
|
126
|
+
escaped = true
|
|
127
|
+
elsif char == quote
|
|
128
|
+
quote = nil
|
|
129
|
+
end
|
|
130
|
+
else
|
|
131
|
+
case char
|
|
132
|
+
when "\"", "'", "`"
|
|
133
|
+
quote = char
|
|
134
|
+
when "{"
|
|
135
|
+
depth += 1
|
|
136
|
+
when "}"
|
|
137
|
+
depth -= 1
|
|
138
|
+
return [expression.strip, index + 1] if depth.zero?
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
expression << char
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
index += 1
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
nil
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
data/lib/klenod/build/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: klenod-build
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.0.
|
|
4
|
+
version: 0.0.24
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Andrés Alin
|
|
@@ -15,14 +15,14 @@ dependencies:
|
|
|
15
15
|
requirements:
|
|
16
16
|
- - '='
|
|
17
17
|
- !ruby/object:Gem::Version
|
|
18
|
-
version: 0.0.
|
|
18
|
+
version: 0.0.24
|
|
19
19
|
type: :runtime
|
|
20
20
|
prerelease: false
|
|
21
21
|
version_requirements: !ruby/object:Gem::Requirement
|
|
22
22
|
requirements:
|
|
23
23
|
- - '='
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
|
-
version: 0.0.
|
|
25
|
+
version: 0.0.24
|
|
26
26
|
- !ruby/object:Gem::Dependency
|
|
27
27
|
name: async
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -293,6 +293,7 @@ files:
|
|
|
293
293
|
- lib/klenod/build/plugins/ruby_plugin.rb
|
|
294
294
|
- lib/klenod/build/plugins/static_asset_plugin.rb
|
|
295
295
|
- lib/klenod/build/plugins/svg_plugin.rb
|
|
296
|
+
- lib/klenod/build/plugins/text_interpolation.rb
|
|
296
297
|
- lib/klenod/build/profiler.rb
|
|
297
298
|
- lib/klenod/build/resolution_error_formatter.rb
|
|
298
299
|
- lib/klenod/build/resolver.rb
|