1/*
2 * Copyright (C) 2010 Google Inc.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.google.clearsilver.jsilver.interpreter;
18
19import com.google.clearsilver.jsilver.autoescape.EscapeMode;
20import com.google.clearsilver.jsilver.data.Data;
21import com.google.clearsilver.jsilver.data.DataContext;
22import com.google.clearsilver.jsilver.exceptions.ExceptionUtil;
23import com.google.clearsilver.jsilver.exceptions.JSilverIOException;
24import com.google.clearsilver.jsilver.exceptions.JSilverInterpreterException;
25import com.google.clearsilver.jsilver.functions.FunctionExecutor;
26import com.google.clearsilver.jsilver.syntax.analysis.DepthFirstAdapter;
27import com.google.clearsilver.jsilver.syntax.node.AAltCommand;
28import com.google.clearsilver.jsilver.syntax.node.AAutoescapeCommand;
29import com.google.clearsilver.jsilver.syntax.node.ACallCommand;
30import com.google.clearsilver.jsilver.syntax.node.ADataCommand;
31import com.google.clearsilver.jsilver.syntax.node.ADefCommand;
32import com.google.clearsilver.jsilver.syntax.node.AEachCommand;
33import com.google.clearsilver.jsilver.syntax.node.AEscapeCommand;
34import com.google.clearsilver.jsilver.syntax.node.AEvarCommand;
35import com.google.clearsilver.jsilver.syntax.node.AHardIncludeCommand;
36import com.google.clearsilver.jsilver.syntax.node.AHardLincludeCommand;
37import com.google.clearsilver.jsilver.syntax.node.AIfCommand;
38import com.google.clearsilver.jsilver.syntax.node.AIncludeCommand;
39import com.google.clearsilver.jsilver.syntax.node.ALincludeCommand;
40import com.google.clearsilver.jsilver.syntax.node.ALoopCommand;
41import com.google.clearsilver.jsilver.syntax.node.ALoopIncCommand;
42import com.google.clearsilver.jsilver.syntax.node.ALoopToCommand;
43import com.google.clearsilver.jsilver.syntax.node.ALvarCommand;
44import com.google.clearsilver.jsilver.syntax.node.ANameCommand;
45import com.google.clearsilver.jsilver.syntax.node.ANameVariable;
46import com.google.clearsilver.jsilver.syntax.node.ASetCommand;
47import com.google.clearsilver.jsilver.syntax.node.AUvarCommand;
48import com.google.clearsilver.jsilver.syntax.node.AVarCommand;
49import com.google.clearsilver.jsilver.syntax.node.AWithCommand;
50import com.google.clearsilver.jsilver.syntax.node.PCommand;
51import com.google.clearsilver.jsilver.syntax.node.PExpression;
52import com.google.clearsilver.jsilver.syntax.node.PPosition;
53import com.google.clearsilver.jsilver.syntax.node.PVariable;
54import com.google.clearsilver.jsilver.syntax.node.TCsOpen;
55import com.google.clearsilver.jsilver.syntax.node.TWord;
56import com.google.clearsilver.jsilver.template.Macro;
57import com.google.clearsilver.jsilver.template.RenderingContext;
58import com.google.clearsilver.jsilver.template.Template;
59import com.google.clearsilver.jsilver.template.TemplateLoader;
60import com.google.clearsilver.jsilver.values.Value;
61import com.google.clearsilver.jsilver.values.VariableValue;
62
63import java.io.IOException;
64import java.util.Iterator;
65import java.util.LinkedList;
66
67/**
68 * Main JSilver interpreter. This walks a template's AST and renders the result out.
69 */
70public class TemplateInterpreter extends DepthFirstAdapter {
71
72  private final Template template;
73
74  private final ExpressionEvaluator expressionEvaluator;
75  private final VariableLocator variableLocator;
76  private final TemplateLoader templateLoader;
77  private final RenderingContext context;
78  private final DataContext dataContext;
79
80  public TemplateInterpreter(Template template, TemplateLoader templateLoader,
81      RenderingContext context, FunctionExecutor functionExecutor) {
82    this.template = template;
83    this.templateLoader = templateLoader;
84    this.context = context;
85    this.dataContext = context.getDataContext();
86
87    expressionEvaluator = new ExpressionEvaluator(dataContext, functionExecutor);
88    variableLocator = new VariableLocator(expressionEvaluator);
89  }
90
91  // ------------------------------------------------------------------------
92  // COMMAND PROCESSING
93
94  /**
95   * Chunk of data (i.e. not a CS command).
96   */
97  @Override
98  public void caseADataCommand(ADataCommand node) {
99    context.writeUnescaped(node.getData().getText());
100  }
101
102  /**
103   * <?cs var:blah > expression. Evaluate as string and write output, using default escaping.
104   */
105  @Override
106  public void caseAVarCommand(AVarCommand node) {
107    setLastPosition(node.getPosition());
108
109    // Evaluate expression.
110    Value value = expressionEvaluator.evaluate(node.getExpression());
111    writeVariable(value);
112  }
113
114  /**
115   * <?cs uvar:blah > expression. Evaluate as string and write output, but don't escape.
116   */
117  @Override
118  public void caseAUvarCommand(AUvarCommand node) {
119    setLastPosition(node.getPosition());
120
121    // Evaluate expression.
122    Value value = expressionEvaluator.evaluate(node.getExpression());
123    context.writeUnescaped(value.asString());
124  }
125
126  /**
127   * <?cs lvar:blah > command. Evaluate expression and execute commands within.
128   */
129  @Override
130  public void caseALvarCommand(ALvarCommand node) {
131    setLastPosition(node.getPosition());
132    evaluateVariable(node.getExpression(), "[lvar expression]");
133  }
134
135  /**
136   * <?cs evar:blah > command. Evaluate expression and execute commands within.
137   */
138  @Override
139  public void caseAEvarCommand(AEvarCommand node) {
140    setLastPosition(node.getPosition());
141    evaluateVariable(node.getExpression(), "[evar expression]");
142  }
143
144  private void evaluateVariable(PExpression expression, String stackTraceDescription) {
145    // Evaluate expression.
146    Value value = expressionEvaluator.evaluate(expression);
147
148    // Now parse result, into new mini template.
149    Template template =
150        templateLoader.createTemp(stackTraceDescription, value.asString(), context
151            .getAutoEscapeMode());
152
153    // Intepret new template.
154    try {
155      template.render(context);
156    } catch (IOException e) {
157      throw new JSilverInterpreterException(e.getMessage());
158    }
159  }
160
161  /**
162   * <?cs linclude!'somefile.cs' > command. Lazily includes another template (at render time).
163   * Throw an error if file does not exist.
164   */
165  @Override
166  public void caseAHardLincludeCommand(AHardLincludeCommand node) {
167    setLastPosition(node.getPosition());
168    include(node.getExpression(), false);
169  }
170
171  /**
172   * <?cs linclude:'somefile.cs' > command. Lazily includes another template (at render time).
173   * Silently ignore if the included file does not exist.
174   */
175  @Override
176  public void caseALincludeCommand(ALincludeCommand node) {
177    setLastPosition(node.getPosition());
178    include(node.getExpression(), true);
179  }
180
181  /**
182   * <?cs include!'somefile.cs' > command. Throw an error if file does not exist.
183   */
184  @Override
185  public void caseAHardIncludeCommand(AHardIncludeCommand node) {
186    setLastPosition(node.getPosition());
187    include(node.getExpression(), false);
188  }
189
190  /**
191   * <?cs include:'somefile.cs' > command. Silently ignore if the included file does not
192   * exist.
193   */
194  @Override
195  public void caseAIncludeCommand(AIncludeCommand node) {
196    setLastPosition(node.getPosition());
197    include(node.getExpression(), true);
198  }
199
200  /**
201   * <?cs set:x='y' > command.
202   */
203  @Override
204  public void caseASetCommand(ASetCommand node) {
205    setLastPosition(node.getPosition());
206    String variableName = variableLocator.getVariableName(node.getVariable());
207
208    try {
209      Data variable = dataContext.findVariable(variableName, true);
210      Value value = expressionEvaluator.evaluate(node.getExpression());
211      variable.setValue(value.asString());
212      // TODO: what about nested structures?
213      // "set" was used to set a variable to a constant or escaped value like
214      // <?cs set: x = "<b>X</b>" ?> or <?cs set: y = html_escape(x) ?>
215      // Keep track of this so autoescaping code can take it into account.
216      variable.setEscapeMode(value.getEscapeMode());
217    } catch (UnsupportedOperationException e) {
218      // An error occurred - probably due to trying to modify an UnmodifiableData
219      throw new UnsupportedOperationException(createUnsupportedOperationMessage(node, context
220          .getIncludedTemplateNames()), e);
221    }
222  }
223
224  /**
225   * &lt;?cs name:blah &gt; command. Writes out the name of the original variable referred to by a
226   * given node.
227   */
228  @Override
229  public void caseANameCommand(ANameCommand node) {
230    setLastPosition(node.getPosition());
231    String variableName = variableLocator.getVariableName(node.getVariable());
232    Data variable = dataContext.findVariable(variableName, false);
233    if (variable != null) {
234      context.writeEscaped(variable.getSymlink().getName());
235    }
236  }
237
238  /**
239   * &lt;?cs if:blah &gt; ... &lt;?cs else &gt; ... &lt;?cs /if &gt; command.
240   */
241  @Override
242  public void caseAIfCommand(AIfCommand node) {
243    setLastPosition(node.getPosition());
244    Value value = expressionEvaluator.evaluate(node.getExpression());
245    if (value.asBoolean()) {
246      node.getBlock().apply(this);
247    } else {
248      node.getOtherwise().apply(this);
249    }
250  }
251
252
253  /**
254   * &lt;?cs escape:'html' &gt; command. Changes default escaping function.
255   */
256  @Override
257  public void caseAEscapeCommand(AEscapeCommand node) {
258    setLastPosition(node.getPosition());
259    Value value = expressionEvaluator.evaluate(node.getExpression());
260    String escapeStrategy = value.asString();
261
262    context.pushEscapingFunction(escapeStrategy);
263    node.getCommand().apply(this);
264    context.popEscapingFunction();
265  }
266
267  /**
268   * A fake command injected by AutoEscaper.
269   *
270   * AutoEscaper determines the html context in which an include or lvar or evar command is called
271   * and stores this context in the AAutoescapeCommand node.
272   */
273  @Override
274  public void caseAAutoescapeCommand(AAutoescapeCommand node) {
275    setLastPosition(node.getPosition());
276    Value value = expressionEvaluator.evaluate(node.getExpression());
277    String escapeStrategy = value.asString();
278
279    EscapeMode mode = EscapeMode.computeEscapeMode(escapeStrategy);
280
281    context.pushAutoEscapeMode(mode);
282    node.getCommand().apply(this);
283    context.popAutoEscapeMode();
284  }
285
286  /**
287   * &lt;?cs with:x=Something &gt; ... &lt;?cs /with &gt; command. Aliases a value within a specific
288   * scope.
289   */
290  @Override
291  public void caseAWithCommand(AWithCommand node) {
292    setLastPosition(node.getPosition());
293    VariableLocator variableLocator = new VariableLocator(expressionEvaluator);
294    String withVar = variableLocator.getVariableName(node.getVariable());
295    Value value = expressionEvaluator.evaluate(node.getExpression());
296
297    if (value instanceof VariableValue) {
298      if (((VariableValue) value).getReference() == null) {
299        // With refers to a non-existent variable. Do nothing.
300        return;
301      }
302    }
303
304    dataContext.pushVariableScope();
305    setTempVariable(withVar, value);
306    node.getCommand().apply(this);
307    dataContext.popVariableScope();
308  }
309
310  /**
311   * &lt;?cs loop:10 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers, starting at
312   * zero.
313   */
314  @Override
315  public void caseALoopToCommand(ALoopToCommand node) {
316    setLastPosition(node.getPosition());
317    int end = expressionEvaluator.evaluate(node.getExpression()).asNumber();
318
319    // Start is always zero, increment is always 1, so end < 0 is invalid.
320    if (end < 0) {
321      return; // Incrementing the wrong way. Avoid infinite loop.
322    }
323
324    loop(node.getVariable(), 0, end, 1, node.getCommand());
325  }
326
327  /**
328   * &lt;?cs loop:0,10 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers.
329   */
330  @Override
331  public void caseALoopCommand(ALoopCommand node) {
332    setLastPosition(node.getPosition());
333    int start = expressionEvaluator.evaluate(node.getStart()).asNumber();
334    int end = expressionEvaluator.evaluate(node.getEnd()).asNumber();
335
336    // Start is always zero, increment is always 1, so end < 0 is invalid.
337    if (end < start) {
338      return; // Incrementing the wrong way. Avoid infinite loop.
339    }
340
341    loop(node.getVariable(), start, end, 1, node.getCommand());
342  }
343
344  /**
345   * &lt;?cs loop:0,10,2 &gt; ... &lt;?cs /loop &gt; command. Loops over a range of numbers, with a
346   * specific increment.
347   */
348  @Override
349  public void caseALoopIncCommand(ALoopIncCommand node) {
350    setLastPosition(node.getPosition());
351    int start = expressionEvaluator.evaluate(node.getStart()).asNumber();
352    int end = expressionEvaluator.evaluate(node.getEnd()).asNumber();
353    int incr = expressionEvaluator.evaluate(node.getIncrement()).asNumber();
354
355    if (incr == 0) {
356      return; // No increment. Avoid infinite loop.
357    }
358    if (incr > 0 && start > end) {
359      return; // Incrementing the wrong way. Avoid infinite loop.
360    }
361    if (incr < 0 && start < end) {
362      return; // Incrementing the wrong way. Avoid infinite loop.
363    }
364
365    loop(node.getVariable(), start, end, incr, node.getCommand());
366  }
367
368  /**
369   * &lt;?cs each:x=Stuff &gt; ... &lt;?cs /each &gt; command. Loops over child items of a data
370   * node.
371   */
372  @Override
373  public void caseAEachCommand(AEachCommand node) {
374    setLastPosition(node.getPosition());
375    Value expression = expressionEvaluator.evaluate(node.getExpression());
376
377    if (expression instanceof VariableValue) {
378      VariableValue variableValue = (VariableValue) expression;
379      Data parent = variableValue.getReference();
380      if (parent != null) {
381        each(node.getVariable(), variableValue.getName(), parent, node.getCommand());
382      }
383    }
384  }
385
386  /**
387   * &lt;?cs alt:someValue &gt; ... &lt;?cs /alt &gt; command. If value exists, write it, otherwise
388   * write the body of the command.
389   */
390  @Override
391  public void caseAAltCommand(AAltCommand node) {
392    setLastPosition(node.getPosition());
393    Value value = expressionEvaluator.evaluate(node.getExpression());
394    if (value.asBoolean()) {
395      writeVariable(value);
396    } else {
397      node.getCommand().apply(this);
398    }
399  }
400
401  private void writeVariable(Value value) {
402    if (template.getEscapeMode().isAutoEscapingMode()) {
403      autoEscapeAndWriteVariable(value);
404    } else if (value.isPartiallyEscaped()) {
405      context.writeUnescaped(value.asString());
406    } else {
407      context.writeEscaped(value.asString());
408    }
409  }
410
411  private void autoEscapeAndWriteVariable(Value value) {
412    if (isTrustedValue(value) || value.isPartiallyEscaped()) {
413      context.writeUnescaped(value.asString());
414    } else {
415      context.writeEscaped(value.asString());
416    }
417  }
418
419  private boolean isTrustedValue(Value value) {
420    // True if PropagateEscapeStatus is enabled and value has either been
421    // escaped or contains a constant string.
422    return context.getAutoEscapeOptions().getPropagateEscapeStatus()
423        && !value.getEscapeMode().equals(EscapeMode.ESCAPE_NONE);
424  }
425
426  // ------------------------------------------------------------------------
427  // MACROS
428
429  /**
430   * &lt;?cs def:someMacro(x,y) &gt; ... &lt;?cs /def &gt; command. Define a macro (available for
431   * the remainder of the interpreter context.
432   */
433  @Override
434  public void caseADefCommand(ADefCommand node) {
435    String macroName = makeWord(node.getMacro());
436    LinkedList<PVariable> arguments = node.getArguments();
437    String[] argumentNames = new String[arguments.size()];
438    int i = 0;
439    for (PVariable argument : arguments) {
440      if (!(argument instanceof ANameVariable)) {
441        throw new JSilverInterpreterException("Invalid name for macro '" + macroName
442            + "' argument " + i + " : " + argument);
443      }
444      argumentNames[i++] = ((ANameVariable) argument).getWord().getText();
445    }
446    // TODO: Should we enforce that macro args can't repeat the same
447    // name?
448    context.registerMacro(macroName, new InterpretedMacro(node.getCommand(), template, macroName,
449        argumentNames, this, context));
450  }
451
452  private String makeWord(LinkedList<TWord> words) {
453    if (words.size() == 1) {
454      return words.getFirst().getText();
455    }
456    StringBuilder result = new StringBuilder();
457    for (TWord word : words) {
458      if (result.length() > 0) {
459        result.append('.');
460      }
461      result.append(word.getText());
462    }
463    return result.toString();
464  }
465
466  /**
467   * &lt;?cs call:someMacro(x,y) command. Call a macro. Need to create a new variable scope to hold
468   * the local variables defined by the parameters of the macro definition
469   */
470  @Override
471  public void caseACallCommand(ACallCommand node) {
472    String macroName = makeWord(node.getMacro());
473    Macro macro = context.findMacro(macroName);
474
475    // Make sure that the number of arguments passed to the macro match the
476    // number expected.
477    if (node.getArguments().size() != macro.getArgumentCount()) {
478      throw new JSilverInterpreterException("Number of arguments to macro " + macroName + " ("
479          + node.getArguments().size() + ") does not match " + "number of expected arguments ("
480          + macro.getArgumentCount() + ")");
481    }
482
483    int numArgs = node.getArguments().size();
484    if (numArgs > 0) {
485      Value[] argValues = new Value[numArgs];
486
487      // We must first evaluate the parameters we are passing or there could be
488      // conflicts if new argument names match existing variables.
489      Iterator<PExpression> argumentValues = node.getArguments().iterator();
490      for (int i = 0; argumentValues.hasNext(); i++) {
491        argValues[i] = expressionEvaluator.evaluate(argumentValues.next());
492      }
493
494      // No need to bother pushing and popping the variable scope stack
495      // if there are no new local variables to declare.
496      dataContext.pushVariableScope();
497
498      for (int i = 0; i < argValues.length; i++) {
499        setTempVariable(macro.getArgumentName(i), argValues[i]);
500      }
501    }
502    try {
503      macro.render(context);
504    } catch (IOException e) {
505      throw new JSilverIOException(e);
506    }
507    if (numArgs > 0) {
508      // No need to bother pushing and popping the variable scope stack
509      // if there are no new local variables to declare.
510      dataContext.popVariableScope();
511    }
512  }
513
514  // ------------------------------------------------------------------------
515  // HELPERS
516  //
517  // Much of the functionality in this section could easily be inlined,
518  // however it makes the rest of the interpreter much easier to understand
519  // and refactor with them defined here.
520
521  private void each(PVariable variable, String parentName, Data items, PCommand command) {
522    // Since HDF variables are now passed to macro parameters by path name
523    // we need to create a path for each child when generating the
524    // VariableValue object.
525    VariableLocator variableLocator = new VariableLocator(expressionEvaluator);
526    String eachVar = variableLocator.getVariableName(variable);
527    StringBuilder pathBuilder = new StringBuilder(parentName);
528    pathBuilder.append('.');
529    int length = pathBuilder.length();
530    dataContext.pushVariableScope();
531    for (Data child : items.getChildren()) {
532      pathBuilder.delete(length, pathBuilder.length());
533      pathBuilder.append(child.getName());
534      setTempVariable(eachVar, Value.variableValue(pathBuilder.toString(), dataContext));
535      command.apply(this);
536    }
537    dataContext.popVariableScope();
538  }
539
540  private void loop(PVariable loopVar, int start, int end, int incr, PCommand command) {
541    VariableLocator variableLocator = new VariableLocator(expressionEvaluator);
542    String varName = variableLocator.getVariableName(loopVar);
543
544    dataContext.pushVariableScope();
545    // Loop deals with counting forward or backwards.
546    for (int index = start; incr > 0 ? index <= end : index >= end; index += incr) {
547      // We reuse the same scope for efficiency and simply overwrite the
548      // previous value of the loop variable.
549      dataContext.createLocalVariableByValue(varName, String.valueOf(index), index == start,
550          index == end);
551
552      command.apply(this);
553    }
554    dataContext.popVariableScope();
555  }
556
557  /**
558   * Code common to all three include commands.
559   *
560   * @param expression expression representing name of file to include.
561   * @param ignoreMissingFile {@code true} if any FileNotFound error generated by the template
562   *        loader should be ignored, {@code false} otherwise.
563   */
564  private void include(PExpression expression, boolean ignoreMissingFile) {
565    // Evaluate expression.
566    Value path = expressionEvaluator.evaluate(expression);
567
568    String templateName = path.asString();
569    if (!context.pushIncludeStackEntry(templateName)) {
570      throw new JSilverInterpreterException(createIncludeLoopErrorMessage(templateName, context
571          .getIncludedTemplateNames()));
572    }
573
574    loadAndRenderIncludedTemplate(templateName, ignoreMissingFile);
575
576    if (!context.popIncludeStackEntry(templateName)) {
577      // Include stack trace is corrupted
578      throw new IllegalStateException("Unable to find on include stack: " + templateName);
579    }
580  }
581
582  private String createIncludeLoopErrorMessage(String templateName, Iterable<String> includeStack) {
583    StringBuilder message = new StringBuilder();
584    message.append("File included twice: ");
585    message.append(templateName);
586
587    message.append(" Include stack:");
588    for (String fileName : includeStack) {
589      message.append("\n -> ");
590      message.append(fileName);
591    }
592    message.append("\n -> ");
593    message.append(templateName);
594    return message.toString();
595  }
596
597  private String createUnsupportedOperationMessage(PCommand node, Iterable<String> includeStack) {
598    StringBuilder message = new StringBuilder();
599
600    message.append("exception thrown while parsing node: ");
601    message.append(node.toString());
602    message.append(" (class ").append(node.getClass().getSimpleName()).append(")");
603    message.append("\nTemplate include stack: ");
604
605    for (Iterator<String> iter = includeStack.iterator(); iter.hasNext();) {
606      message.append(iter.next());
607      if (iter.hasNext()) {
608        message.append(" -> ");
609      }
610    }
611    message.append("\n");
612
613    return message.toString();
614  }
615
616  // This method should ONLY be called from include()
617  private void loadAndRenderIncludedTemplate(String templateName, boolean ignoreMissingFile) {
618    // Now load new template with given name.
619    Template template = null;
620    try {
621      template =
622          templateLoader.load(templateName, context.getResourceLoader(), context
623              .getAutoEscapeMode());
624    } catch (RuntimeException e) {
625      if (ignoreMissingFile && ExceptionUtil.isFileNotFoundException(e)) {
626        return;
627      } else {
628        throw e;
629      }
630    }
631
632    // Intepret loaded template.
633    try {
634      // TODO: Execute lincludes (but not includes) in a separate
635      // context.
636      template.render(context);
637    } catch (IOException e) {
638      throw new JSilverInterpreterException(e.getMessage());
639    }
640  }
641
642  private void setLastPosition(PPosition position) {
643    // Walks position node which will eventually result in calling
644    // caseTCsOpen().
645    position.apply(this);
646  }
647
648  /**
649   * Every time a &lt;cs token is found, grab the line and position (for helpful error messages).
650   */
651  @Override
652  public void caseTCsOpen(TCsOpen node) {
653    int line = node.getLine();
654    int column = node.getPos();
655    context.setCurrentPosition(line, column);
656  }
657
658  private void setTempVariable(String variableName, Value value) {
659    if (value instanceof VariableValue) {
660      // If the value is a Data variable name, then we store a reference to its
661      // name as discovered by the expression evaluator and resolve it each
662      // time for correctness.
663      dataContext.createLocalVariableByPath(variableName, ((VariableValue) value).getName());
664    } else {
665      dataContext.createLocalVariableByValue(variableName, value.asString(), value.getEscapeMode());
666    }
667  }
668
669}
670