glaemscribe 1.1.14 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
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