glaemscribe 1.1.14 → 1.2.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.
Files changed (55) hide show
  1. checksums.yaml +5 -5
  2. data/bin/glaemscribe +19 -15
  3. data/glaemresources/charsets/cirth_ds.cst +205 -0
  4. data/glaemresources/charsets/sarati_eldamar.cst +256 -0
  5. data/glaemresources/charsets/tengwar_ds_annatar.cst +546 -0
  6. data/glaemresources/charsets/tengwar_ds_eldamar.cst +535 -0
  7. data/glaemresources/charsets/tengwar_ds_elfica.cst +551 -0
  8. data/glaemresources/charsets/tengwar_ds_parmaite.cst +534 -0
  9. data/glaemresources/charsets/tengwar_ds_sindarin.cst +531 -0
  10. data/glaemresources/charsets/tengwar_freemono.cst +217 -0
  11. data/glaemresources/charsets/tengwar_guni_annatar.cst +628 -0
  12. data/glaemresources/charsets/tengwar_guni_eldamar.cst +618 -0
  13. data/glaemresources/charsets/tengwar_guni_elfica.cst +620 -0
  14. data/glaemresources/charsets/tengwar_guni_parmaite.cst +621 -0
  15. data/glaemresources/charsets/tengwar_guni_sindarin.cst +617 -0
  16. data/glaemresources/charsets/tengwar_telcontar.cst +218 -0
  17. data/glaemresources/charsets/unicode_gothic.cst +64 -0
  18. data/glaemresources/charsets/unicode_runes.cst +121 -0
  19. data/glaemresources/modes/{adunaic.glaem → adunaic-tengwar-glaemscrafu.glaem} +14 -2
  20. data/glaemresources/modes/{blackspeech.glaem → blackspeech-tengwar-general_use.glaem} +12 -2
  21. data/glaemresources/modes/japanese-tengwar.glaem +771 -0
  22. data/glaemresources/modes/{khuzdul.glaem → khuzdul-cirth-moria.glaem} +4 -1
  23. data/glaemresources/modes/{futhorc.glaem → old_english-futhorc.glaem} +0 -0
  24. data/glaemresources/modes/{mercian.glaem → old_english-tengwar-mercian.glaem} +22 -12
  25. data/glaemresources/modes/{westsaxon.glaem → old_english-tengwar-westsaxon.glaem} +20 -11
  26. data/glaemresources/modes/{futhark-runicus.glaem → old_norse-futhark-runicus.glaem} +0 -0
  27. data/glaemresources/modes/{futhark-younger.glaem → old_norse-futhark-younger.glaem} +0 -0
  28. data/glaemresources/modes/{quenya.glaem → quenya-tengwar-classical.glaem} +32 -50
  29. data/glaemresources/modes/raw-tengwar.glaem +46 -23
  30. data/glaemresources/modes/{rlyehian.glaem → rlyehian-tengwar.glaem} +14 -3
  31. data/glaemresources/modes/{sindarin-daeron.glaem → sindarin-cirth-daeron.glaem} +55 -14
  32. data/glaemresources/modes/{sindarin-beleriand.glaem → sindarin-tengwar-beleriand.glaem} +154 -28
  33. data/glaemresources/modes/{sindarin.glaem → sindarin-tengwar-general_use.glaem} +86 -25
  34. data/glaemresources/modes/{telerin.glaem → telerin-tengwar-glaemscrafu.glaem} +16 -6
  35. data/glaemresources/modes/{westron.glaem → westron-tengwar-glaemscrafu.glaem} +18 -8
  36. data/lib/api/charset.rb +67 -7
  37. data/lib/api/charset_parser.rb +7 -0
  38. data/lib/api/constants.rb +3 -4
  39. data/lib/api/fragment.rb +26 -5
  40. data/lib/api/if_tree.rb +70 -8
  41. data/lib/api/macro.rb +40 -0
  42. data/lib/api/mode.rb +35 -13
  43. data/lib/api/mode_parser.rb +106 -12
  44. data/lib/api/object_additions.rb +23 -1
  45. data/lib/api/option.rb +17 -2
  46. data/lib/api/post_processor/resolve_virtuals.rb +25 -9
  47. data/lib/api/resource_manager.rb +1 -0
  48. data/lib/api/rule_group.rb +170 -26
  49. data/lib/api/sheaf_chain_iterator.rb +1 -1
  50. data/lib/api/transcription_processor.rb +3 -3
  51. data/lib/api/tts.rb +51 -0
  52. data/lib/glaemscribe.rb +34 -31
  53. data/lib_espeak/espeakng.for.glaemscribe.nowasm.sync.js +21 -0
  54. data/lib_espeak/glaemscribe_tts.js +365 -0
  55. metadata +67 -21
@@ -41,7 +41,7 @@ module Glaemscribe
41
41
  EQUIVALENCE_RX_OUT = /(\(.*?\))/
42
42
  EQUIVALENCE_RX_IN = /\((.*?)\)/
43
43
 
44
- # Should pass a fragment expression, e.g. : "h(a)(i)"
44
+ # Should pass a fragment expression, e.g. : "h(a)(i)"
45
45
  def initialize(sheaf, expression)
46
46
  @sheaf = sheaf
47
47
  @mode = sheaf.mode
@@ -49,16 +49,16 @@ module Glaemscribe
49
49
  @expression = expression
50
50
 
51
51
  # Split the fragment, turn it into an array of arrays, e.g. [[h],[a,ä],[i,ï]]
52
- equivalences = expression.split(EQUIVALENCE_RX_OUT).map{ |eq| eq.strip }
52
+ equivalences = expression.split(EQUIVALENCE_RX_OUT).map{ |eq| eq.strip }.reject{ |eq| eq == '' }
53
53
  equivalences = equivalences.map{ |eq|
54
54
  eq =~ EQUIVALENCE_RX_IN
55
55
  if $1
56
56
  eq = $1.split(EQUIVALENCE_SEPARATOR,-1).map{ |elt|
57
57
  elt = elt.strip
58
- elt.split(/\s/)
59
- }
58
+ elt.split(/\s/).map{ |leaf| finalize_fragment_leaf(leaf) }
59
+ }
60
60
  else
61
- eq = [eq.split(/\s/)] # This equivalence has only one possibility
61
+ eq = [eq.split(/\s/).map{ |leaf| finalize_fragment_leaf(leaf) }] # This equivalence has only one possibility
62
62
  end
63
63
  }
64
64
 
@@ -87,6 +87,7 @@ module Glaemscribe
87
87
  # Calculate all combinations for this fragment (productize the array of arrays)
88
88
  res = equivalences[0]
89
89
 
90
+ # ((eq0 x eq1) x eq2) x eq3 ) ... )))))
90
91
  (equivalences.length-1).times { |i|
91
92
  prod = res.product(equivalences[i+1]).map{ |x,y| x+y}
92
93
  res = prod
@@ -95,6 +96,26 @@ module Glaemscribe
95
96
  @combinations = res
96
97
  end
97
98
 
99
+ def finalize_fragment_leaf(leaf)
100
+ if src?
101
+
102
+ # Replace {UNI_XXXX} by its value to allow any unicode char to be found in the transcription tree
103
+ leaf = leaf.gsub(RuleGroup::UNICODE_VAR_NAME_REGEXP_OUT) { |cap_var|
104
+ unival = $1
105
+ new_char = [unival.hex].pack("U")
106
+ new_char = "\u0001" if new_char == '_'
107
+ new_char
108
+ }
109
+
110
+ # Replace '_' (word boundary) by '\u0000' to allow
111
+ # the real underscore to be used in the transcription tree
112
+ # (Do it after replacing the uni_xxx vars because they have underscores inside)
113
+ leaf = leaf.gsub(WORD_BOUNDARY_LANG, WORD_BOUNDARY_TREE)
114
+ leaf = leaf.gsub("\u0001","_")
115
+ end
116
+
117
+ leaf
118
+ end
98
119
 
99
120
  def p
100
121
  ret = "---- " + @expression + "\n"
@@ -24,14 +24,36 @@ module Glaemscribe
24
24
  module API
25
25
  module IfTree
26
26
 
27
+ # A branching if condition
27
28
  class IfCond
28
29
  attr_accessor :line, :expression, :parent_if_term, :child_code_block
29
30
  def initialize(line, parent_if_term, expression)
30
31
  @parent_if_term = parent_if_term
31
32
  @expression = expression
32
33
  end
34
+ def offset
35
+ parent_if_term.offset + " "
36
+ end
37
+ def prefix
38
+ offset + "|-"
39
+ end
40
+ def inspect
41
+ "#{prefix} IF #{expression}\n" +
42
+ "#{child_code_block.inspect}"
43
+ end
44
+ end
45
+
46
+ # A line of code
47
+ class CodeLine
48
+ attr_accessor :expression, :line
49
+ def initialize(expression, line)
50
+ @expression = expression
51
+ @line = line
52
+ end
33
53
  end
34
54
 
55
+ # A node (code lines / preprocessor operators / ... )
56
+ # A node may have children or not depending on their nature
35
57
  class Term
36
58
  attr_accessor :parent_code_block
37
59
  def initialize(parent_code_block)
@@ -43,24 +65,30 @@ module Glaemscribe
43
65
  def is_pre_post_processor_operators?
44
66
  false
45
67
  end
68
+ def is_macro_deploy?
69
+ false
70
+ end
71
+ def offset
72
+ parent_code_block.offset + " "
73
+ end
74
+ def prefix
75
+ offset + "|- "
76
+ end
46
77
  end
47
78
 
79
+ # A ifterm may have multiple ifconds (if,elsif,elsif,...,else)
48
80
  class IfTerm < Term
49
81
  attr_accessor :if_conds
50
82
  def initialize(parent_code_block)
51
83
  super(parent_code_block)
52
84
  @if_conds = []
53
85
  end
54
- end
55
-
56
- class CodeLine
57
- attr_accessor :expression, :line
58
- def initialize(expression, line)
59
- @expression = expression
60
- @line = line
86
+ def inspect
87
+ "#{prefix} CONDITIONAL BLOCK\n" +
88
+ @if_conds.map{ |c| c.inspect }.join("\n")
61
89
  end
62
90
  end
63
-
91
+
64
92
  class PrePostProcessorOperatorsTerm < Term
65
93
  attr_accessor :operators
66
94
  def initialize(parent_code_block)
@@ -70,6 +98,9 @@ module Glaemscribe
70
98
  def is_pre_post_processor_operators?
71
99
  true
72
100
  end
101
+ def inspect
102
+ "#{prefix} OPERATORS (#{@operators.count})"
103
+ end
73
104
  end
74
105
 
75
106
  class CodeLinesTerm < Term
@@ -81,6 +112,25 @@ module Glaemscribe
81
112
  def is_code_lines?
82
113
  true
83
114
  end
115
+ def inspect
116
+ "#{prefix} CODE LINES (#{@code_lines.count})"
117
+ end
118
+ end
119
+
120
+ class MacroDeployTerm < Term
121
+ attr_accessor :macro, :line, :arg_value_expressions
122
+ def initialize(macro, line, parent_code_block, arg_value_expressions)
123
+ super(parent_code_block)
124
+ @line = line
125
+ @macro = macro
126
+ @arg_value_expressions = arg_value_expressions
127
+ end
128
+ def is_macro_deploy?
129
+ true
130
+ end
131
+ def inspect
132
+ "#{prefix} MACRO DEPLOY (#{macro.name})"
133
+ end
84
134
  end
85
135
 
86
136
  class CodeBlock
@@ -89,6 +139,18 @@ module Glaemscribe
89
139
  @parent_if_cond = parent_if_cond
90
140
  @terms = []
91
141
  end
142
+ def offset
143
+ ((parent_if_cond)?(parent_if_cond.offset):("")) + " "
144
+ end
145
+ def prefix
146
+ offset + "|- "
147
+ end
148
+ def inspect
149
+ ret = ""
150
+ ret += "|-ROOT\n" if !parent_if_cond
151
+ ret += "#{prefix} Code block\n" +
152
+ @terms.map{|t| t.inspect}.join("\n")
153
+ end
92
154
  end
93
155
 
94
156
  end
@@ -0,0 +1,40 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # Glǽmscribe (also written Glaemscribe) is a software dedicated to
4
+ # the transcription of texts between writing systems, and more
5
+ # specifically dedicated to the transcription of J.R.R. Tolkien's
6
+ # invented languages to some of his devised writing systems.
7
+ #
8
+ # Copyright (C) 2015 Benjamin Babut (Talagan).
9
+ #
10
+ # This program is free software: you can redistribute it and/or modify
11
+ # it under the terms of the GNU Affero General Public License as published by
12
+ # the Free Software Foundation, either version 3 of the License, or
13
+ # any later version.
14
+ #
15
+ # This program is distributed in the hope that it will be useful,
16
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
17
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18
+ # GNU Affero General Public License for more details.
19
+ #
20
+ # You should have received a copy of the GNU Affero General Public License
21
+ # along with this program. If not, see <http://www.gnu.org/licenses/>.
22
+
23
+ module Glaemscribe
24
+ module API
25
+ class Macro
26
+ attr_reader :name, :rule_group, :mode, :arg_names
27
+
28
+ attr_reader :root_code_block
29
+
30
+ def initialize(rule_group,name,arg_names)
31
+ @rule_group = rule_group
32
+ @mode = rule_group.mode
33
+ @name = name
34
+ @arg_names = arg_names
35
+ @root_code_block = IfTree::CodeBlock.new
36
+ end
37
+
38
+ end
39
+ end
40
+ end
@@ -41,8 +41,13 @@ module Glaemscribe
41
41
 
42
42
  attr_accessor :world, :invention
43
43
 
44
+ attr_accessor :has_tts
45
+ attr_reader :current_tts_voice
46
+
44
47
  attr_reader :latest_option_values
45
48
 
49
+
50
+
46
51
  def initialize(name)
47
52
  @name = name
48
53
  @errors = []
@@ -50,6 +55,8 @@ module Glaemscribe
50
55
  @supported_charsets = {}
51
56
  @options = {}
52
57
  @last_raw_options = nil
58
+ @has_tts = false
59
+ @current_tts_voice = nil
53
60
 
54
61
  @pre_processor = TranscriptionPreProcessor.new(self)
55
62
  @processor = TranscriptionProcessor.new(self)
@@ -95,7 +102,7 @@ module Glaemscribe
95
102
 
96
103
  trans_options_converted = {}
97
104
 
98
- # Do a conversion to values space
105
+ # Do a conversion from names to values space
99
106
  trans_options.each{ |oname,valname|
100
107
  trans_options_converted[oname] = @options[oname].value_for_value_name(valname)
101
108
  }
@@ -117,7 +124,13 @@ module Glaemscribe
117
124
  @processor.finalize(@latest_option_values)
118
125
 
119
126
  raw_mode.finalize options if raw_mode
120
-
127
+
128
+ # Update the current espeak voice
129
+ if @has_tts
130
+ espeak_option = @options['espeak_voice'].value_name_for_value(@latest_option_values['espeak_voice'])
131
+ @current_tts_voice = TTS.option_name_to_voice(espeak_option)
132
+ end
133
+
121
134
  self
122
135
  end
123
136
 
@@ -128,16 +141,18 @@ module Glaemscribe
128
141
  @raw_mode = loaded_raw_mode.deep_clone
129
142
  end
130
143
 
131
- def replace_specials(l)
132
- l.
133
- gsub("_",SPECIAL_CHAR_UNDERSCORE).
134
- gsub("\u00a0",SPECIAL_CHAR_NBSP)
135
- end
136
-
137
144
  def strict_transcribe(content, charset = nil)
138
145
  charset = default_charset if !charset
139
146
  return false, "*** No charset usable for transcription. Failed!" if !charset
140
147
 
148
+ if has_tts
149
+ begin
150
+ content = TTS.ipa(content, @current_tts_voice, (raw_mode != nil) )['ipa']
151
+ rescue StandardError => e
152
+ return false, "TTS pre-transcription failed : #{e}."
153
+ end
154
+ end
155
+
141
156
  # Parser works line by line
142
157
  ret = content.lines.map{ |l|
143
158
  restore_lf = false
@@ -146,7 +161,6 @@ module Glaemscribe
146
161
  restore_lf = true
147
162
  end
148
163
  l = @pre_processor.apply(l)
149
- l = replace_specials(l)
150
164
  l = @processor.apply(l)
151
165
  l = @post_processor.apply(l, charset)
152
166
  l += "\n" if restore_lf
@@ -163,12 +177,20 @@ module Glaemscribe
163
177
  chunks.each{ |c|
164
178
  if c =~ /{{(.*?)}}/m
165
179
  succ, r = raw_mode.strict_transcribe($1,charset)
166
- res = res && succ
167
- ret += r if succ
180
+
181
+ if !succ
182
+ return false, r # Propagate error
183
+ end
184
+
185
+ ret += r
168
186
  else
169
187
  succ, r = strict_transcribe(c,charset)
170
- res = res && succ
171
- ret += r if succ
188
+
189
+ if !succ
190
+ return false, r # Propagate error
191
+ end
192
+
193
+ ret += r
172
194
  end
173
195
  }
174
196
  return res,ret
@@ -94,7 +94,13 @@ module Glaemscribe
94
94
  ifcond
95
95
  end
96
96
 
97
- def traverse_if_tree(root_code_block, root_element, text_procedure, element_procedure)
97
+ def traverse_if_tree(context, text_procedure, element_procedure)
98
+
99
+ owner = context[:owner] # The root object of the if tree
100
+ root_element = context[:root_element] # The glaeml root_element of that if tree
101
+ rule_group = context[:rule_group] # The rule group in which this traversal happens (may be null for pre/post processors)
102
+
103
+ root_code_block = owner.root_code_block
98
104
  current_parent_code_block = root_code_block
99
105
 
100
106
  root_element.children.each{ |child|
@@ -145,7 +151,65 @@ module Glaemscribe
145
151
  end
146
152
 
147
153
  current_parent_code_block = if_term.parent_code_block
154
+ when 'macro'
155
+
156
+ # Macro definition, cannot be defined in conditional blocks
157
+ if current_parent_code_block.parent_if_cond || root_element.name != "rules"
158
+ @mode.errors << Glaeml::Error.new(child.line, "Macros can only defined in the 'rules' scope, not in a conditional block (because they are replaced and used at parsing time) or a macro block (local macros are not handled).")
159
+ return
160
+ end
161
+
162
+ if !child.args || child.args.count == 0
163
+ @mode.errors << Glaeml::Error.new(child.line, "Macro misses a name.")
164
+ return
165
+ end
166
+
167
+ macro_args = child.args.clone
168
+ macro_name = macro_args.shift
169
+ macro_args.each{ |arg|
170
+ if(!arg =~ /[0-9A-Z_]+/)
171
+ @mode.errors << Glaeml::Error.new(child.line, "Macro argument name #{arg} has wrong format.")
172
+ return
173
+ end
174
+ }
175
+
176
+ if rule_group.macros[macro_name]
177
+ @mode.errors << Glaeml::Error.new(child.line, "Redefining macro #{macro_name}.")
178
+ return
179
+ end
148
180
 
181
+ macro = Macro.new(rule_group,macro_name,macro_args)
182
+ macro_context = {:owner => macro, :root_element => child, :rule_group => rule_group}
183
+ traverse_if_tree(macro_context, text_procedure, element_procedure)
184
+
185
+ rule_group.macros[macro_name] = macro
186
+
187
+ when 'deploy'
188
+
189
+ if !rule_group
190
+ @mode.errors << Glaeml::Error.new(child.line, "Macros can only be deployed in a rule group.")
191
+ return
192
+ end
193
+
194
+ macro_args = child.args.clone
195
+ macro_name = macro_args.shift
196
+ macro = rule_group.macros[macro_name]
197
+
198
+ if !macro
199
+ @mode.errors << Glaeml::Error.new(child.line, "Macro '#{macro_name}' not found in rule group '#{rule_group.name}'.")
200
+ return
201
+ end
202
+
203
+ wanted_argcount = macro.arg_names.count
204
+ given_argcount = macro_args.count
205
+ if wanted_argcount != given_argcount
206
+ @mode.errors << Glaeml::Error.new(child.line, "Macro '#{macro_name}' takes #{wanted_argcount} arguments, not #{given_argcount}.")
207
+ return
208
+ end
209
+
210
+ macro_node = IfTree::MacroDeployTerm.new(macro, child.line, current_parent_code_block, macro_args)
211
+ current_parent_code_block.terms << macro_node
212
+
149
213
  else
150
214
  # Do something with this child element
151
215
  element_procedure.call(current_parent_code_block, child)
@@ -184,10 +248,13 @@ module Glaemscribe
184
248
  term.operators << operator_class.new(element.clone)
185
249
  end
186
250
  }
187
-
188
- root_code_block = ((pre_not_post)?(@mode.pre_processor.root_code_block):(@mode.post_processor.root_code_block))
189
-
190
- self.traverse_if_tree(root_code_block, processor_element, text_procedure, element_procedure )
251
+
252
+ processor_context = {
253
+ owner: ((pre_not_post)?(@mode.pre_processor):(@mode.post_processor)),
254
+ root_element: processor_element,
255
+ rule_group: nil
256
+ }
257
+ traverse_if_tree(processor_context, text_procedure, element_procedure )
191
258
  end
192
259
 
193
260
  def parse(file_path, mode_options = {})
@@ -219,6 +286,7 @@ module Glaemscribe
219
286
  doc.root_node.gpath("options.option").each{ |option_element|
220
287
  values = {}
221
288
  visibility = nil
289
+ is_radio = false
222
290
 
223
291
  option_element.gpath("value").each{ |value_element|
224
292
  value_name = value_element.args.first
@@ -227,6 +295,8 @@ module Glaemscribe
227
295
  option_element.gpath("visible_when").each{ |visible_element|
228
296
  visibility = visible_element.args.first
229
297
  }
298
+
299
+ option_element.gpath('radio').each{|e| is_radio = true}
230
300
 
231
301
  option_name_at = option_element.args[0]
232
302
  option_default_val_at = option_element.args[1]
@@ -236,8 +306,9 @@ module Glaemscribe
236
306
  @mode.errors << Glaeml::Error.new(option_element.line, "Missing option default value.")
237
307
  end
238
308
 
239
- option = Option.new(@mode, option_name_at, option_default_val_at, values, visibility)
240
- @mode.options[option.name] = option
309
+ option = Option.new(@mode, option_name_at, option_default_val_at, values, option_element.line, visibility)
310
+ option.is_radio = is_radio
311
+ @mode.options[option.name] = option
241
312
  }
242
313
 
243
314
  # Read the supported font list
@@ -301,6 +372,7 @@ module Glaemscribe
301
372
 
302
373
  lcount = element.line
303
374
  element.args[0].lines.to_a.each{ |l|
375
+ # Split into lines of code and count the lines
304
376
  l = l.strip
305
377
  term.code_lines << IfTree::CodeLine.new(l, lcount)
306
378
  lcount += 1
@@ -310,13 +382,35 @@ module Glaemscribe
310
382
  element_procedure = Proc.new { |current_parent_code_block, element|
311
383
  # This is fatal.
312
384
  @mode.errors << Glaeml::Error.new(element.line, "Unknown directive #{element.name}.")
313
- }
314
-
315
- self.traverse_if_tree( rule_group.root_code_block, rules_element, text_procedure, element_procedure )
385
+ }
386
+
387
+ processor_context = {
388
+ owner: rule_group,
389
+ root_element: rules_element,
390
+ rule_group: rule_group
391
+ }
392
+ traverse_if_tree(processor_context, text_procedure, element_procedure )
316
393
  }
317
-
318
- @mode.finalize(mode_options) if !@mode.errors.any?
394
+
395
+
396
+ espeak_option = @mode.options['espeak_voice']
397
+ if espeak_option
398
+ # Singleton lazy load the TTS engine
399
+ # If the mode relies on espeak
400
+ TTS::load_engine
401
+ @mode.has_tts = true
402
+
403
+ # Check if all voices are supported
404
+ espeak_option.values.keys.each { |vname|
405
+ voice = TTS::option_name_to_voice(vname)
406
+ if !(TTS::voice_list.include? voice)
407
+ @mode.errors << Glaeml::Error.new(espeak_option.line, "Option has unhandled voice #{voice}.")
408
+ end
409
+ }
410
+ end
319
411
 
412
+ @mode.finalize(mode_options) if !@mode.errors.any?
413
+
320
414
  @mode
321
415
  end
322
416
  end