quby-compiler 0.5.15 → 0.5.17

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 126c7a3a5ef150e332a888dfef85acc264a46fe4a886b2f5ef9b0a19f460b14d
4
- data.tar.gz: 9bc6fad59d9da077cca02ab744bed4497dea3764e09f82d4aaf42016f7479dbc
3
+ metadata.gz: 4be07e6f3fabe1a0b02fbc36ea63c5ccd964519aedb08d0adaf9b6196f4f9830
4
+ data.tar.gz: 382ea4dc3bd640a7d2e92b9bb27ad1bd5b1176129eb93e84fc71ad954bb691f9
5
5
  SHA512:
6
- metadata.gz: 183182375a43d83e98bb4667865c78b958fb2443d8f028ea4411b59d2caaf894825072d764753539811bc659d68a8294c284d4226b7dddfdfb56635049a60a01
7
- data.tar.gz: '066826a89ee2dd03f8dc8b76b2d284a583331e4e3181823935d9afb3e48f970a8aeeece44ca7b6bc6f479ccd3ee179b47b39a65b26e60fef5489efb8106b41d3'
6
+ metadata.gz: 1e699567c3318dcc2b4cf9d7b2b44e7c05cacab8783f17e5631bfcee12d1de690aba8ab8ae3e9da93bab07be680a26f4fe9ca735af8c27ea7bbd72b79146aad9
7
+ data.tar.gz: 8ffbc7c1374528de5edefd0e35cab356c07e98b0e2f6436b0291510be9d0751971261846c7b98300290fc12689dd6a961816de16b1cebf0ff90bf6f0bce70d29
data/CHANGELOG.md CHANGED
@@ -1,7 +1,29 @@
1
+ # 0.5.17
2
+
3
+ * Add option#label.
4
+ * quby.json
5
+ * Add option#label to option#description just in case.
6
+ * quby2.json:
7
+ * Pass options#hidden.
8
+ * Allow hr tags in prose.
9
+ * Allow all prose tags in question#description, except headers.
10
+ * Allow css_vars to be defined on questionnaire and question level.
11
+ * Add option#label
12
+ * quby2.json: Add option#label (without fallback for now, until quby2 0.9.7 is deployed everywhere).
13
+
14
+ # 0.5.16
15
+
16
+ * add integer as split_to_units option, with units and conversions as attributes
17
+ * add sexp_variables to do calculations using s expressions, for now in quby2, but later in the backend.
18
+ * test that select/radio options are always numeric
19
+ * quby2.json
20
+ * add split_to_units question, that saves a integer.
21
+ * Calculate sexpr variables and allow strings to interpolate them {{calculation.some_var}}
22
+
1
23
  # 0.5.15
2
24
 
3
25
  * Add context_description to questions, to have a text item that is hidden together with the question.
4
- * quuby2.json
26
+ * quby2.json
5
27
  * Moved quby2 serialization to the serializer, compact everything, no markdown for v2 title/descriptions.
6
28
  * Sanitize Quby2 html in v2.
7
29
  * Add contextDescription to questions.
@@ -0,0 +1,16 @@
1
+ merge(values(:v_1, :v_2), values(v_4).recode(1 => 3)).sum
2
+
3
+
4
+
5
+ divide(value(:v_2), multiply(value(:v_1), value(:v_1)))
6
+
7
+ w / l * l
8
+
9
+
10
+ sexp_variable :key1 do
11
+ foo = values(:v_1, :v_2)
12
+ bar = recode(foo, 1 => 3)
13
+ bla = merge(foo, bar)
14
+ sum(bla)
15
+
16
+ end
@@ -8,6 +8,7 @@ require 'quby/compiler/dsl/charting/line_chart_builder'
8
8
  require 'quby/compiler/dsl/charting/radar_chart_builder'
9
9
  require 'quby/compiler/dsl/charting/bar_chart_builder'
10
10
  require 'quby/compiler/dsl/charting/overview_chart_builder'
11
+ require 'quby/compiler/dsl/sexp_variable_builder'
11
12
 
12
13
  require_relative 'standardized_panel_generators'
13
14
 
@@ -139,6 +140,12 @@ module Quby
139
140
  @questionnaire.extra_css += value
140
141
  end
141
142
 
143
+ # css_vars("option-gap-y" => 1)
144
+ def css_vars(value)
145
+ @questionnaire.css_vars ||= {}
146
+ @questionnaire.css_vars.merge!(value)
147
+ end
148
+
142
149
  def allow_switch_to_bulk(value=true)
143
150
  @questionnaire.allow_switch_to_bulk = value
144
151
  end
@@ -200,6 +207,10 @@ module Quby
200
207
  end
201
208
  end
202
209
 
210
+ def sexp_variable(key, &block)
211
+ @questionnaire.add_sexp_variable(key, SexpVariableBuilder.new(key, &block).build)
212
+ end
213
+
203
214
  # variable :totaal do
204
215
  # # Plain old Ruby code here, executed in the scope of the answer
205
216
  # # variables are private to the score calculation
@@ -12,6 +12,7 @@ module Quby
12
12
  end
13
13
 
14
14
  def build
15
+ @question.after_build
15
16
  @question
16
17
  end
17
18
 
@@ -14,6 +14,16 @@ module Quby
14
14
  super
15
15
  @question = Entities::Questions::IntegerQuestion.new(key, options)
16
16
  end
17
+
18
+ # as split_to_units
19
+ def units(*values)
20
+ @question.units = values
21
+ end
22
+
23
+ # as split_to_units
24
+ def conversions(value)
25
+ @question.conversions = value
26
+ end
17
27
  end
18
28
  end
19
29
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'quby/compiler/entities/sexp_variable'
4
+ module Quby::Compiler
5
+ module DSL
6
+ # sexp_variable :myvar do
7
+ # sum(number_values(:v_1, :v_2))
8
+ # end
9
+ class ::SexpVariableBuilder
10
+ attr_reader :calculation, :key
11
+
12
+ def initialize(key, &block)
13
+ @key = key
14
+ @calculation = instance_eval(&block)
15
+ end
16
+
17
+ def string_value(key)
18
+ Entities::SexpVariables::StringValue.new(op: :string_value, key: key)
19
+ end
20
+
21
+ def number_value(key)
22
+ Entities::SexpVariables::NumberValue.new(op: :number_value, key: key)
23
+ end
24
+
25
+ def number_values(*keys)
26
+ keys.map { |key| number_value(key) }
27
+ end
28
+
29
+ %i[sum subtract multiply divide].each do |op|
30
+ define_method(op) do |*values|
31
+ Entities::SexpVariables::NumberReducer.new(op:, values: wrap_and_flatten(values))
32
+ end
33
+ end
34
+
35
+ def round(value)
36
+ Entities::SexpVariables::NumberMethod.new(op: :round, value: value)
37
+ end
38
+
39
+ def build
40
+ Entities::SexpVariable.new(key:, calculation:)
41
+ end
42
+
43
+ private
44
+
45
+ def wrap_and_flatten(values)
46
+ values.flat_map { |value|
47
+ case value
48
+ when Numeric
49
+ Entities::SexpVariables::Number.new(op: :number, value: value)
50
+ else
51
+ value
52
+ end
53
+ }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -30,6 +30,9 @@ module Quby
30
30
  # How should we display this question
31
31
  attr_accessor :as
32
32
 
33
+ # extra styling options
34
+ attr_accessor :css_vars
35
+
33
36
  # To hide old questions
34
37
  attr_accessor :hidden
35
38
  validates :hidden, inclusion: {in: [true, false, nil], message: "must be boolean"}
@@ -164,6 +167,7 @@ module Quby
164
167
  @cols = options[:cols] || 40
165
168
  @default_invisible = options[:default_invisible] || false
166
169
  @labels ||= []
170
+ @css_vars = options[:css_vars]
167
171
 
168
172
  @col_span = options[:col_span] || 1
169
173
  @row_span = options[:row_span] || 1
@@ -208,6 +212,10 @@ module Quby
208
212
  end
209
213
  # rubocop:enable CyclomaticComplexity, Metrics/MethodLength
210
214
 
215
+ # called after DSL has instance_evalled everything within the question block.
216
+ def after_build
217
+ end
218
+
211
219
  def context_free_title_or_title
212
220
  context_free_title || title
213
221
  end
@@ -11,6 +11,8 @@ module Quby
11
11
 
12
12
  attr_reader :key
13
13
  attr_reader :value
14
+ validates :value, numericality: {allow_nil: true} # nil for checkbox questions.
15
+ attr_reader :label
14
16
  attr_reader :description, :context_free_description
15
17
  attr_reader :questions
16
18
  # for scale/radio/checbox questions, piece of of html that is rendered between the options
@@ -31,6 +33,7 @@ module Quby
31
33
  @key = key
32
34
  @question = question
33
35
  @value = options[:value]
36
+ @label = options[:label]
34
37
  @description = options[:description]
35
38
  @context_free_description = options[:context_free_description]
36
39
  @questions = []
@@ -40,6 +40,7 @@ module Quby
40
40
  @license = :unknown
41
41
  @layout_version = nil
42
42
  @extra_css = ""
43
+ @css_vars = nil
43
44
  @allow_switch_to_bulk = false
44
45
  @panels = []
45
46
  @flags = {}.with_indifferent_access
@@ -53,6 +54,7 @@ module Quby
53
54
  @outcome_tables = []
54
55
  @check_score_keys_consistency = true
55
56
  @lookup_tables = {}
57
+ @sexp_variables = {}
56
58
  @versions = []
57
59
  @seeds_patch = {}
58
60
  @anonymous_conditions = Entities::AnonymousConditions.new
@@ -76,6 +78,7 @@ module Quby
76
78
  attr_writer :leave_page_alert
77
79
  attr_reader :fields
78
80
  attr_accessor :extra_css
81
+ attr_accessor :css_vars
79
82
  attr_accessor :allow_switch_to_bulk
80
83
  attr_reader :license
81
84
  attr_accessor :licensor
@@ -102,6 +105,7 @@ module Quby
102
105
 
103
106
  attr_accessor :outcome_tables
104
107
  attr_accessor :score_schemas
108
+ attr_accessor :sexp_variables
105
109
  attr_accessor :lookup_tables
106
110
  attr_accessor :anonymous_conditions
107
111
 
@@ -362,6 +366,10 @@ module Quby
362
366
  end
363
367
  end
364
368
 
369
+ def add_sexp_variable(key, sexp_variable)
370
+ sexp_variables[key] = sexp_variable
371
+ end
372
+
365
373
  def add_outcome_table(outcome_table_options)
366
374
  outcome_tables << OutcomeTable.new(**outcome_table_options, questionnaire: self)
367
375
  end
@@ -0,0 +1,58 @@
1
+ module Quby::Compiler::Entities::Questions::Concerns
2
+ module SplitToUnits
3
+ extend ActiveSupport::Concern
4
+ DEFAULT_SPLIT_TO_UNIT_CONVERSIONS = {
5
+ minutes: {
6
+ hours: 60,
7
+ days: 1440,
8
+ weeks: 10080
9
+ },
10
+ seconds: {
11
+ minutes: 60,
12
+ hours: 3600,
13
+ days: 86400
14
+ },
15
+ m: {
16
+ km: 1000
17
+ },
18
+ cm: {
19
+ m: 100
20
+ },
21
+ mm: {
22
+ cm: 10,
23
+ m: 1000
24
+ },
25
+ g: {
26
+ kg: 1000
27
+ }
28
+ }.freeze
29
+
30
+ included do
31
+ attr_accessor :units, :conversions
32
+
33
+ validates :units, :conversions, presence: true, if: -> { as == :split_to_units }
34
+ validate :validate_split_to_units, if: -> { as == :split_to_units }
35
+ end
36
+
37
+ def after_build
38
+ super
39
+ return unless as == :split_to_units
40
+ @unit = units&.last
41
+ (@conversions ||= {}).reverse_merge!(default_split_to_units_conversions)
42
+ end
43
+
44
+ def default_split_to_units_conversions
45
+ (DEFAULT_SPLIT_TO_UNIT_CONVERSIONS[unit] || {}).slice(*units)
46
+ end
47
+
48
+ def validate_split_to_units
49
+ return unless units.present? && conversions.present?
50
+
51
+ (units - [unit]).each do |unit_to_convert|
52
+ if conversions[unit_to_convert].nil?
53
+ errors.add(:conversions, "should contain a conversion for unit #{unit_to_convert}")
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'concerns/slider'
4
+ require_relative 'concerns/split_to_units'
4
5
 
5
6
  module Quby
6
7
  module Compiler
@@ -8,6 +9,7 @@ module Quby
8
9
  module Questions
9
10
  class IntegerQuestion < Question
10
11
  include Concerns::Slider
12
+ include Concerns::SplitToUnits
11
13
 
12
14
  def size
13
15
  @size || 30
@@ -0,0 +1,50 @@
1
+ require 'quby/compiler/entities/sexp_variables'
2
+
3
+ module Quby::Compiler::Entities
4
+ class SexpVariable
5
+ attr_reader :calculation, :key
6
+
7
+ def initialize(key:, calculation:)
8
+ @key = key
9
+ @calculation = calculation
10
+ end
11
+
12
+ # Called by DefinitionValidator.
13
+ def validate(questionnaire)
14
+ case calculation
15
+ when SexpVariables::NumberValue
16
+ validate_question_exist(questionnaire, calculation)
17
+ validate_value_is_number(questionnaire, calculation)
18
+ when SexpVariables::StringValue
19
+ validate_question_exist(questionnaire, calculation)
20
+ validate_value_is_string(questionnaire, calculation)
21
+ end
22
+ end
23
+
24
+ def validate_question_exist(questionnaire, sexp)
25
+ return if questionnaire.question_hash.key?(sexp.key)
26
+
27
+ fail "sexp_variable #{key} uses nonexistent question #{sexp.key}."
28
+ end
29
+
30
+ def validate_value_is_number(questionnaire, sexp)
31
+ question = questionnaire.question_hash[sexp.key]
32
+ case question
33
+ when Questions::IntegerQuestion, Questions::FloatQuestion, Questions::SelectQuestion, Questions::RadioQuestion
34
+ return
35
+ end
36
+
37
+ fail "sexp_variable #{key} uses non-numeric question #{sexp.key} for number_value."
38
+ end
39
+
40
+ def validate_value_is_string(questionnaire, sexp)
41
+ question = questionnaire.question_hash[sexp.key]
42
+ case question
43
+ when Questions::StringQuestion, Questions::TextareaQuestion
44
+ return
45
+ end
46
+
47
+ fail "sexp_variable #{key} uses non-string question #{sexp.key} for string_value."
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,41 @@
1
+ module Quby::Compiler::Entities
2
+ module SexpVariables
3
+ class Base < Dry::Struct
4
+ end
5
+
6
+ # recursive dependencies, we so we define the classes first.
7
+ class NumberValue < Base; end
8
+ class Number < Base; end
9
+ class NumberReducer < Base; end
10
+ class NumberMethod < Base; end
11
+
12
+ NumericType = Number | NumberReducer | NumberMethod | NumberValue
13
+
14
+ class NumberValue
15
+ attribute :op, Quby::Types::Symbol.enum(:number_value)
16
+ attribute :key, Quby::Types::Symbol
17
+ end
18
+
19
+ class StringValue < Base
20
+ attribute :op, Quby::Types::Symbol.enum(:string_value)
21
+ attribute :key, Quby::Types::Symbol
22
+ end
23
+
24
+ class Number
25
+ attribute :op, Quby::Types::Symbol.enum(:value)
26
+ attribute :value, Quby::Types::Integer | Quby::Types::Float
27
+ end
28
+
29
+ # returning a Number
30
+ class NumberReducer
31
+ attribute :op, Quby::Types::Symbol.enum(:sum, :subtract, :multiply, :divide)
32
+ attribute :values, Quby::Types::Array.of(NumericType)
33
+ end
34
+
35
+ # returning a Number
36
+ class NumberMethod
37
+ attribute :op, Quby::Types::Symbol.enum(:round)
38
+ attribute :value, NumericType
39
+ end
40
+ end
41
+ end
@@ -192,7 +192,7 @@ module Quby
192
192
  {
193
193
  key: option.key,
194
194
  value: option.value,
195
- description: option.description,
195
+ description: option.label ? "#{option.label} #{option.description}" : option.description,
196
196
  context_free_description: option.context_free_description,
197
197
  questions: option.questions.map {|question| question_as_json(question)},
198
198
  inner_title: option.inner_title,
@@ -23,7 +23,9 @@ module Quby
23
23
  questions: questions,
24
24
  textvars: textvars,
25
25
  validations: validations,
26
- visibilityRules: visibility_rules.as_json
26
+ visibilityRules: visibility_rules.as_json,
27
+ sexpVariables: sexp_variables,
28
+ cssVars: css_vars,
27
29
  }
28
30
  end
29
31
 
@@ -83,10 +85,17 @@ module Quby
83
85
  end
84
86
 
85
87
  def float_question(question)
86
- integer_question(question)
88
+ number_question(question)
87
89
  end
88
90
 
89
91
  def integer_question(question)
92
+ {
93
+ **number_question(question),
94
+ **split_to_units_question(question),
95
+ }.compact
96
+ end
97
+
98
+ def number_question(question)
90
99
  {
91
100
  **base_question(question),
92
101
  **slider_question(question),
@@ -94,8 +103,8 @@ module Quby
94
103
  maximum: question.maximum,
95
104
  size: size(question),
96
105
  unit: question.as != :slider && question.unit,
97
- }.compact
98
- end
106
+ }.compact
107
+ end
99
108
 
100
109
  def radio_question(question)
101
110
  {
@@ -141,6 +150,7 @@ module Quby
141
150
  description: handle_html(question.description, type: :question_description),
142
151
  contextDescription: handle_html(question.context_description, type: :prose, v1_markdown: false),
143
152
  type: question_type(question),
153
+ cssVars: question.css_vars,
144
154
  hidden: question.hidden?,
145
155
  displayModes: question.display_modes,
146
156
  viewSelector: question.view_selector,
@@ -165,6 +175,13 @@ module Quby
165
175
  }
166
176
  end
167
177
 
178
+ def split_to_units_question(question)
179
+ {
180
+ units: question.units,
181
+ conversions: question.conversions,
182
+ }
183
+ end
184
+
168
185
  def question_type(question)
169
186
  {
170
187
  date: 'date_parts',
@@ -200,8 +217,10 @@ module Quby
200
217
  type: 'option',
201
218
  key: option.key,
202
219
  value: option.question.type != :check_box && option.value,
220
+ label: option.label, # TODO fallback on description and empty description if no label (after quby2 has been deployed)
203
221
  description: option.question.type == :select ? option.description : handle_html(option.description),
204
- questions: option.question.type != :select && option.questions.map{ question(_1) },
222
+ questions: option.question.type != :select && option.questions.map{ question(_1) },
223
+ hidden: option.hidden.presence,
205
224
  viewId: option.view_id
206
225
  }.compact
207
226
  end
@@ -237,9 +256,9 @@ module Quby
237
256
  when :simple
238
257
  html_sanitizer.sanitize(html, tags: %w[strong em sup sub br span], attributes: %w[class])
239
258
  when :question_description
240
- html_sanitizer.sanitize(html, tags: %w[strong em b i u sup sub p span br ul ol li], attributes: %w[class])
259
+ html_sanitizer.sanitize(html, tags: %w[strong em b i u sup sub pre blockquote p span br ul ol li a], attributes: %w[class])
241
260
  when :prose
242
- html_sanitizer.sanitize(html, tags: %w[strong em b i u sup sub pre blockquote p span br ul ol li a h1 h2 h3 h4], attributes: %w[href class target])
261
+ html_sanitizer.sanitize(html, tags: %w[strong em b i u sup sub pre blockquote p span br ul ol li a h1 h2 h3 h4 hr], attributes: %w[href class target])
243
262
  end
244
263
  elsif v1_markdown
245
264
  Quby::Compiler::MarkdownParser.new(html).to_html
@@ -26,6 +26,7 @@ module Quby
26
26
  validate_outcome_tables(questionnaire)
27
27
  validate_markdown_fields(questionnaire) if questionnaire.validate_html
28
28
  validate_raw_content_items(questionnaire) if questionnaire.validate_html
29
+ validate_sexp_variables(questionnaire)
29
30
  # Some compilation errors are Exceptions (pure syntax errors) and some StandardErrors (NameErrors)
30
31
  rescue Exception => exception # rubocop:disable Lint/RescueException
31
32
  definition.errors.add(:sourcecode, message: "Questionnaire error: #{definition.key}\n" \
@@ -364,6 +365,12 @@ scores_schema tables to the resulting seed."
364
365
  fail "#{key || html} contains invalid html: #{fragment.errors.map(&:to_s).join(', ')}."
365
366
  end
366
367
 
368
+ def validate_sexp_variables(questionnaire)
369
+ questionnaire.sexp_variables.each_value do |sexp_variable|
370
+ sexp_variable.validate(questionnaire)
371
+ end
372
+ end
373
+
367
374
  def delete_prefix(key, questionnaire)
368
375
  key.delete_prefix("#{questionnaire.key}_")
369
376
  end
@@ -1,5 +1,5 @@
1
1
  module Quby
2
2
  module Compiler
3
- VERSION = "0.5.15"
3
+ VERSION = "0.5.17"
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: quby-compiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.15
4
+ version: 0.5.17
5
5
  platform: ruby
6
6
  authors:
7
7
  - Marten Veldthuis
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-02-27 00:00:00.000000000 Z
10
+ date: 2025-04-16 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activemodel
@@ -150,6 +150,7 @@ files:
150
150
  - lib/quby/compiler/dsl/charting/overview_chart_builder.rb
151
151
  - lib/quby/compiler/dsl/charting/radar_chart_builder.rb
152
152
  - lib/quby/compiler/dsl/helpers.rb
153
+ - lib/quby/compiler/dsl/merge(values(:v_1, :v_2), values(v_4).rb
153
154
  - lib/quby/compiler/dsl/panel_builder.rb
154
155
  - lib/quby/compiler/dsl/question_builder.rb
155
156
  - lib/quby/compiler/dsl/questionnaire_builder.rb
@@ -165,6 +166,7 @@ files:
165
166
  - lib/quby/compiler/dsl/questions/text_question_builder.rb
166
167
  - lib/quby/compiler/dsl/score_builder.rb
167
168
  - lib/quby/compiler/dsl/score_schema_builder.rb
169
+ - lib/quby/compiler/dsl/sexp_variable_builder.rb
168
170
  - lib/quby/compiler/dsl/standardized_panel_generators.rb
169
171
  - lib/quby/compiler/dsl/table_builder.rb
170
172
  - lib/quby/compiler/entities.rb
@@ -188,6 +190,7 @@ files:
188
190
  - lib/quby/compiler/entities/questionnaire.rb
189
191
  - lib/quby/compiler/entities/questions/checkbox_question.rb
190
192
  - lib/quby/compiler/entities/questions/concerns/slider.rb
193
+ - lib/quby/compiler/entities/questions/concerns/split_to_units.rb
191
194
  - lib/quby/compiler/entities/questions/date_question.rb
192
195
  - lib/quby/compiler/entities/questions/deprecated_question.rb
193
196
  - lib/quby/compiler/entities/questions/float_question.rb
@@ -198,6 +201,8 @@ files:
198
201
  - lib/quby/compiler/entities/questions/text_question.rb
199
202
  - lib/quby/compiler/entities/score_calculation.rb
200
203
  - lib/quby/compiler/entities/score_schema.rb
204
+ - lib/quby/compiler/entities/sexp_variable.rb
205
+ - lib/quby/compiler/entities/sexp_variables.rb
201
206
  - lib/quby/compiler/entities/subscore_schema.rb
202
207
  - lib/quby/compiler/entities/table.rb
203
208
  - lib/quby/compiler/entities/text.rb