1 package org.daisy.pipeline.css.speech;
2
3 import java.util.AbstractList;
4 import java.util.ArrayList;
5 import java.util.EnumSet;
6 import java.util.ListIterator;
7 import java.util.List;
8 import java.util.Optional;
9
10 import cz.vutbr.web.css.Declaration;
11 import cz.vutbr.web.css.RuleFactory;
12 import cz.vutbr.web.css.SupportedCSS;
13 import cz.vutbr.web.css.Term;
14 import cz.vutbr.web.css.TermIdent;
15 import cz.vutbr.web.css.TermInteger;
16 import cz.vutbr.web.css.TermList;
17 import cz.vutbr.web.css.TermString;
18 import cz.vutbr.web.csskit.OutputUtil;
19 import cz.vutbr.web.csskit.RuleFactoryImpl;
20 import cz.vutbr.web.domassign.SupportedCSS21;
21
22 import org.daisy.braille.css.BrailleCSSParserFactory;
23 import org.daisy.pipeline.css.CssSerializer;
24 import org.daisy.pipeline.css.impl.UnmodifiableTerm;
25
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28
29 public class VoiceFamilyList extends AbstractList<Term<?>> implements TermList {
30
31 private static final String PROPERTY = "voice-family";
32
33
34 private static final BrailleCSSParserFactory parserFactory = new BrailleCSSParserFactory();
35 private static final RuleFactory ruleFactory = RuleFactoryImpl.getInstance();
36 private static final SupportedCSS speechCSS = SupportedCSS21.getInstance();
37 private static final Logger logger = LoggerFactory.getLogger(VoiceFamilyList.class);
38
39 public static VoiceFamilyList of(String list) throws IllegalArgumentException {
40 Declaration d = parserFactory.parseDeclaration(PROPERTY + ": " + list);
41 if (!PROPERTY.equals(d.getProperty()))
42 throw new IllegalArgumentException();
43 return of(d);
44 }
45
46 public static VoiceFamilyList of(List<Term<?>> list) throws IllegalArgumentException {
47 List<VoiceFamily> l = new ArrayList<>();
48 ListIterator<Term<?>> i = list.listIterator();
49 boolean ageWithoutGender = false;
50 while (i.hasNext()) {
51 VoiceFamily f = VoiceFamily.of(i);
52 l.add(f);
53 ageWithoutGender = ageWithoutGender || (f.age != null && f.gender == null);
54 }
55
56
57
58
59
60
61 if (l.size() > 1 && l.size() <= 3) {
62 String name = null;
63 Age age = null;
64 Gender gender = null;
65 boolean extra = false;
66 for (VoiceFamily f : l) {
67 if (name == null && f.name != null && f.gender == null && f.age == null)
68 name = f.name;
69 else if (gender == null && f.gender != null && f.name == null && f.age == null)
70 gender = f.gender;
71 else if (age == null && f.age != null && f.name == null && f.gender == null)
72 age = f.age;
73 else {
74 extra = true;
75 break;
76 }
77 }
78 if (!extra) {
79 String oldSyntax = CssSerializer.serializeTermList(l);
80 l.clear();
81 if (age != null && gender == null) {
82
83
84 l.add(new VoiceFamily(name, age, Gender.MALE, null, null));
85 l.add(new VoiceFamily(name, age, Gender.FEMALE, null, Operator.COMMA));
86 ageWithoutGender = false;
87 } else {
88 l.add(new VoiceFamily(name, age, gender, null, null));
89 }
90 logger.warn("Use `font-family: " + CssSerializer.serializeTermList(l)
91 + "` instead of `font-family: " + oldSyntax + "`");
92 }
93 }
94
95
96
97
98 if (ageWithoutGender) {
99 List<VoiceFamily> l2 = new ArrayList<>();
100 for (VoiceFamily f : l) {
101 if (f.age != null && f.gender == null) {
102 logger.warn("Use `font-family: " + f.age + " male, " + f.age + " female` "
103 + "instead of `font-family: " + f.age + "`");
104 l2.add(new VoiceFamily(f.name, f.age, Gender.MALE, f.variant, f.comma));
105 l2.add(new VoiceFamily(f.name, f.age, Gender.FEMALE, f.variant, Operator.COMMA));
106 } else
107 l2.add(f);
108 }
109 l = l2;
110 }
111 return new VoiceFamilyList(l);
112 }
113
114 private List<VoiceFamily> list;
115
116 private VoiceFamilyList(List<VoiceFamily> list) {
117 super();
118 this.list = list;
119 }
120
121 @Override
122 public String toString() {
123 return CssSerializer.toString(this);
124 }
125
126 @Override
127 public VoiceFamily get(int index) {
128 return list.get(index);
129 }
130
131 @Override
132 public int size() {
133 return list.size();
134 }
135
136 @Override
137 public List<Term<?>> getValue() {
138 return this;
139 }
140
141 @Override
142 public Operator getOperator() {
143 return null;
144 }
145
146 @Override
147 public VoiceFamilyList shallowClone() {
148 try {
149 return (VoiceFamilyList)super.clone();
150 } catch (CloneNotSupportedException e) {
151 throw new InternalError("coding error");
152 }
153 }
154
155 @Override
156 public VoiceFamilyList clone() {
157 VoiceFamilyList clone = shallowClone();
158 clone.list = new ArrayList<>(list);
159 return clone;
160 }
161
162 @Override
163 public VoiceFamilyList setValue(List<Term<?>> value) {
164 throw new UnsupportedOperationException("Unmodifiable");
165 }
166
167 @Override
168 public VoiceFamilyList setOperator(Operator operator) {
169 throw new UnsupportedOperationException("Unmodifiable");
170 }
171
172 public static class VoiceFamily extends UnmodifiableTerm<VoiceFamily> {
173
174 private static VoiceFamily of(ListIterator<Term<?>> list) throws IllegalArgumentException {
175 if (!list.hasNext())
176 throw new IllegalArgumentException();
177 Term<?> t = list.next();
178 if (t instanceof VoiceFamily)
179 return (VoiceFamily)t;
180 Operator comma = t.getOperator();
181 if (comma == Operator.SPACE)
182 comma = null;
183 else if (comma != null && comma != Operator.COMMA)
184 throw new IllegalArgumentException();
185 String name = null;
186 Age age = null;
187 Integer ageAsInt = null;
188 Gender gender = null;
189 Integer variant = null;
190 do {
191 if (name != null)
192 throw new IllegalArgumentException();
193 if (ageAsInt != null)
194 throw new IllegalArgumentException();
195 if (gender == null) {
196 if (t instanceof TermIdent) {
197 if (age == null) {
198 try {
199 age = Age.of((TermIdent)t);
200 if (!age.toString().equals(((TermIdent)t).getValue())) {
201 age = null;
202 name = ((TermIdent)t).getValue();
203 }
204 } catch (IllegalArgumentException e) {
205 try {
206 gender = Gender.of((TermIdent)t);
207 } catch (IllegalArgumentException ee) {
208 name = ((TermIdent)t).getValue();
209 }
210 }
211 } else {
212 try {
213 gender = Gender.of((TermIdent)t);
214 } catch (IllegalArgumentException e) {
215 name = ((TermIdent)t).getValue();
216 }
217 }
218 } else if (t instanceof TermString) {
219 name = ((TermString)t).getValue();
220 } else if (t instanceof TermInteger) {
221 if (age == null) {
222 ageAsInt = ((TermInteger)t).getIntValue();
223 if (ageAsInt < 0)
224 throw new IllegalArgumentException();
225 } else
226 throw new IllegalArgumentException();
227 } else
228 throw new IllegalArgumentException();
229 } else if (variant == null && t instanceof TermInteger) {
230 variant = ((TermInteger)t).getIntValue();
231 if (variant <= 0)
232 throw new IllegalArgumentException();
233 } else if (t instanceof TermIdent || t instanceof TermString) {
234 name = ((Term<String>)t).getValue();
235 } else
236 throw new IllegalArgumentException();
237 if (!list.hasNext())
238 break;
239 t = list.next();
240 Operator o = t.getOperator();
241 if (o == Operator.COMMA) {
242 list.previous();
243 break;
244 } else if (o != null && o != Operator.SPACE)
245 throw new IllegalArgumentException();
246 } while (true);
247 if (ageAsInt != null) {
248 if (ageAsInt <= 16)
249 age = Age.CHILD;
250 else if (ageAsInt >= 70)
251 age = Age.OLD;
252 else
253 age = Age.YOUNG;
254 logger.warn("Specifying age as number is deprecated. "
255 + "Use `font-family: " + age + "` instead of `font-family: " + ageAsInt + "`");
256 }
257 if (name == null && gender == null && age == null)
258 throw new IllegalArgumentException();
259 return new VoiceFamily(name, age, gender, variant, comma);
260 }
261
262 private final String name;
263 private final Age age;
264 private final Gender gender;
265 private final Integer variant;
266 private final Operator comma;
267
268 private VoiceFamily(String name, Age age, Gender gender, Integer variant, Operator comma) {
269 this.name = name;
270 this.age = age;
271 this.gender = gender;
272 this.variant = variant;
273 this.comma = comma;
274 }
275
276 public Optional<String> getFamilyName() {
277 return Optional.ofNullable(name);
278 }
279
280 public Optional<Age> getAge() {
281 return Optional.ofNullable(age);
282 }
283
284 public Optional<Gender> getGender() {
285 return Optional.ofNullable(gender);
286 }
287
288 public Optional<Integer> getVariant() {
289 return Optional.ofNullable(variant);
290 }
291
292 @Override
293 public String toString() {
294 StringBuilder s = new StringBuilder();
295 if (comma != null) s.append(", ");
296 if (age != null || gender != null) {
297 if (age != null)
298 s.append(age.toString());
299
300
301 if (gender != null) {
302 if (age != null) s.append(" ");
303 s.append(gender.toString());
304 }
305 if (variant != null)
306 s.append(" " + variant);
307 if (name != null)
308 s.append(" ");
309 }
310 if (name != null)
311 s.append("'" + name.replaceAll("\n", "\\\\A ").replaceAll("'", "\\\\27 ") + "'");
312 return s.toString();
313 }
314
315 @Override
316 public Operator getOperator() {
317 return comma;
318 }
319 }
320
321 public static enum Age {
322 CHILD,
323 YOUNG,
324 OLD;
325
326 private static Age of(TermIdent age) throws IllegalArgumentException {
327 return of(age.getValue());
328 }
329
330 private static Age of(String age) throws IllegalArgumentException {
331 if (age == null)
332 throw new IllegalArgumentException();
333 age = age.toLowerCase();
334 for (Age a : EnumSet.allOf(Age.class))
335 if (a.toString().equals(age))
336 return a;
337 throw new IllegalArgumentException("Not an age: " + age);
338 }
339
340 @Override
341 public String toString() {
342 return name().toLowerCase();
343 }
344 }
345
346 public static enum Gender {
347 MALE,
348 FEMALE,
349 NEUTRAL;
350
351 private static Gender of(TermIdent gender) throws IllegalArgumentException {
352 return of(gender.getValue());
353 }
354
355 private static Gender of(String gender) throws IllegalArgumentException {
356 if (gender == null)
357 throw new IllegalArgumentException();
358 gender = gender.toLowerCase();
359 for (Gender g : EnumSet.allOf(Gender.class))
360 if (g.toString().equals(gender))
361 return g;
362 throw new IllegalArgumentException("Not a gender: " + gender);
363 }
364
365 @Override
366 public String toString() {
367 return name().toLowerCase();
368 }
369 }
370 }