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 * <?cs name:blah > 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 * <?cs if:blah > ... <?cs else > ... <?cs /if > 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 * <?cs escape:'html' > 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 * <?cs with:x=Something > ... <?cs /with > 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 * <?cs loop:10 > ... <?cs /loop > 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 * <?cs loop:0,10 > ... <?cs /loop > 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 * <?cs loop:0,10,2 > ... <?cs /loop > 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 * <?cs each:x=Stuff > ... <?cs /each > 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 * <?cs alt:someValue > ... <?cs /alt > 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 * <?cs def:someMacro(x,y) > ... <?cs /def > 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 * <?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 <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