mirror of
https://github.com/OlafvdSpek/ctemplate.git
synced 2025-10-12 20:19:04 +08:00
1681 lines
65 KiB
C++
1681 lines
65 KiB
C++
// Copyright (c) 2006, Google Inc.
|
|
// All rights reserved.
|
|
//
|
|
// Redistribution and use in source and binary forms, with or without
|
|
// modification, are permitted provided that the following conditions are
|
|
// met:
|
|
//
|
|
// * Redistributions of source code must retain the above copyright
|
|
// notice, this list of conditions and the following disclaimer.
|
|
// * Redistributions in binary form must reproduce the above
|
|
// copyright notice, this list of conditions and the following disclaimer
|
|
// in the documentation and/or other materials provided with the
|
|
// distribution.
|
|
// * Neither the name of Google Inc. nor the names of its
|
|
// contributors may be used to endorse or promote products derived from
|
|
// this software without specific prior written permission.
|
|
//
|
|
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
// ---
|
|
// Author: Frank H. Jernigan
|
|
//
|
|
|
|
#include "config.h"
|
|
|
|
// Needed for pthread_rwlock_*. If it causes problems, you could take
|
|
// it out, but then you'd have to unset HAVE_RWLOCK (at least on linux).
|
|
#define _XOPEN_SOURCE 500 // needed to get the rwlock calls
|
|
|
|
#include <assert.h>
|
|
#include <stdlib.h>
|
|
#include <errno.h>
|
|
#include <time.h>
|
|
#include <ctype.h> // for isspace()
|
|
#include <sys/stat.h>
|
|
#include <unistd.h> // for stat() and open()
|
|
#include <string.h>
|
|
#if defined(HAVE_PTHREAD) && !defined(NO_THREADS)
|
|
# include <pthread.h>
|
|
#endif
|
|
#include <iostream> // for logging
|
|
#include <iomanip> // for indenting in Dump()
|
|
#include <string>
|
|
#include <list>
|
|
#include <vector>
|
|
#include <utility> // for pair
|
|
#include <google/ctemplate/hash_map.h>
|
|
#include "google/template.h"
|
|
#include "google/template_dictionary.h"
|
|
|
|
_START_GOOGLE_NAMESPACE_
|
|
|
|
using std::endl;
|
|
using std::string;
|
|
using std::list;
|
|
using std::vector;
|
|
using std::pair;
|
|
using HASH_NAMESPACE::hash_map;
|
|
using HASH_NAMESPACE::hash;
|
|
|
|
const int kIndent = 2; // num spaces to indent each level
|
|
|
|
#define SAFE_PTHREAD(fncall) do { if ((fncall) != 0) abort(); } while (0)
|
|
|
|
#if defined(HAVE_PTHREAD) && !defined(NO_THREADS)
|
|
# ifdef HAVE_RWLOCK
|
|
// Easiest to use a wrapper class for the read-write mutex
|
|
class RWLock {
|
|
public:
|
|
RWLock() { SAFE_PTHREAD(pthread_rwlock_init(&lock_, NULL)); }
|
|
~RWLock() { SAFE_PTHREAD(pthread_rwlock_destroy(&lock_)); }
|
|
void LockRO() { SAFE_PTHREAD(pthread_rwlock_rdlock(&lock_)); }
|
|
void LockRW() { SAFE_PTHREAD(pthread_rwlock_wrlock(&lock_)); }
|
|
void Unlock() { SAFE_PTHREAD(pthread_rwlock_unlock(&lock_)); }
|
|
private:
|
|
pthread_rwlock_t lock_;
|
|
};
|
|
# else
|
|
// Not as efficient, but the best we can do if !HAVE_RWLOCK
|
|
class RWLock {
|
|
public:
|
|
RWLock() { SAFE_PTHREAD(pthread_mutex_init(&lock_, NULL)); }
|
|
~RWLock() { SAFE_PTHREAD(pthread_mutex_destroy(&lock_)); }
|
|
void LockRO() { SAFE_PTHREAD(pthread_mutex_lock(&lock_)); }
|
|
void LockRW() { SAFE_PTHREAD(pthread_mutex_lock(&lock_)); }
|
|
void Unlock() { SAFE_PTHREAD(pthread_mutex_unlock(&lock_)); }
|
|
private:
|
|
pthread_mutex_t lock_;
|
|
};
|
|
# endif
|
|
|
|
// Mutexes protecting the globals below. First protects g_use_current_dict
|
|
// and template_root_directory_, second protects g_template_cache.
|
|
static pthread_mutex_t g_static_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
static pthread_mutex_t g_cache_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
// This protects vars_seen in WriteOneHeaderEntry, below.
|
|
static pthread_mutex_t g_header_mutex = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
# define LOCK(m) SAFE_PTHREAD(pthread_mutex_lock(m))
|
|
# define UNLOCK(m) SAFE_PTHREAD(pthread_mutex_unlock(m))
|
|
#else
|
|
class RWLock { // since mutex_ has this type
|
|
public:
|
|
RWLock() {}
|
|
void LockRO() {}
|
|
void LockRW() {}
|
|
void Unlock() {}
|
|
};
|
|
# define LOCK(m)
|
|
# define UNLOCK(m)
|
|
#endif
|
|
|
|
namespace {
|
|
|
|
const char * const kDefaultTemplateDirectory = "./";
|
|
const char * const kMainSectionName = "__MAIN__";
|
|
static vector<TemplateDictionary*>* g_use_current_dict; // vector == {NULL}
|
|
|
|
// Type, var, and mutex used to store template objects in the internal cache
|
|
class TemplateCacheHash {
|
|
public:
|
|
hash<const char *> string_hash_;
|
|
TemplateCacheHash() : string_hash_() {}
|
|
bool operator()(const pair<string, Strip>& p) const {
|
|
// Using + here is silly, but should work ok in practice
|
|
return string_hash_(p.first.c_str()) + static_cast<int>(p.second);
|
|
}
|
|
};
|
|
typedef hash_map<pair<string, Strip>, Template*, TemplateCacheHash>
|
|
TemplateCache;
|
|
static TemplateCache *g_template_cache;
|
|
|
|
// A very simple logging system
|
|
static int kVerbosity = 0; // you can change this by hand to get vlogs
|
|
#define LOG(level) std::cerr << #level ": "
|
|
#define VLOG(level) if (kVerbosity >= level) std::cerr << "V" #level ": "
|
|
|
|
#define LOG_TEMPLATE_NAME(severity, template) \
|
|
LOG(severity) << "Template " << template->template_file() << ": "
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Modifiers
|
|
// We allow variables to have modifiers, each possibly with a value
|
|
// associated with it. Format is
|
|
// {{VARNAME:modname[=modifier-value]:modname[=modifier-value]:...}}
|
|
// Modname refers to a functor that takes the variable's value
|
|
// and modifier-value (empty-string if no modifier-value was
|
|
// specified), and returns a munged values. Modifiers are applied
|
|
// left-to-right. We define the legal modnames here, and the
|
|
/// functors they refer to.
|
|
//
|
|
// Modifiers have a long-name, an optional short-name (one char;
|
|
// may be \0 if you don't want a shortname), and a functor that's
|
|
// applied to the variable. The functor always takes a string and
|
|
// a nonce (the modifier-value) as input, even if value_status is
|
|
// MODVAL_FORBIDDEN.
|
|
// ----------------------------------------------------------------------
|
|
|
|
enum ModvalStatus { MODVAL_FORBIDDEN, MODVAL_OPTIONAL, MODVAL_REQUIRED };
|
|
|
|
// I wanted to use functors and subclassing, but I couldn't get it to
|
|
// work -- it always called the superclass operator() -- so I just use
|
|
// function pointers. Presumably it's just as efficient in practice.
|
|
// Downside is you can't subclass these guys to keep around state. :-(
|
|
typedef string (*ModifierFunctor)(const string& input, const string& nonce);
|
|
|
|
struct Modifier {
|
|
const char* long_name;
|
|
char short_name;
|
|
ModvalStatus value_status;
|
|
ModifierFunctor modifier;
|
|
};
|
|
|
|
typedef vector< pair<ModifierFunctor, string> > ModifierAndNonces;
|
|
|
|
static string HtmlModifier(const string& input, const string&) {
|
|
return TemplateDictionary::html_escape(input);
|
|
}
|
|
|
|
static string JavascriptModifier(const string& input, const string&) {
|
|
return TemplateDictionary::javascript_escape(input);
|
|
}
|
|
|
|
static const Modifier g_modifiers[] = {
|
|
{ "html_escape", 'h', MODVAL_FORBIDDEN, &HtmlModifier },
|
|
{ "javascript_escape", 'j', MODVAL_FORBIDDEN, &JavascriptModifier },
|
|
};
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// PathJoin()
|
|
// Joins a and b together to form a path. If 'b' starts with '/'
|
|
// then we just return b, otherwise a + b. If 'a' does not end in
|
|
// a slash we put a slash in the middle. Does *not* resolve ..'s
|
|
// and stuff like that, for now. Not very efficient.
|
|
// Returns a string which is the joining.
|
|
// ----------------------------------------------------------------------
|
|
|
|
static string PathJoin(const string& a, const string& b) {
|
|
if (b.empty()) return a; // degenerate case 1
|
|
if (a.empty()) return b; // degenerate case 2
|
|
if (b[0] == '/') return b; // absolute path
|
|
if (a[a.length()-1] == '/') return a + b; // 'well-formed' case
|
|
return a + '/' + b;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// WriteOneHeaderEntry()
|
|
// This dumps information about a template that is useful to
|
|
// make_tpl_varnames_h -- information about the variable and
|
|
// section names used in a template, so we can define constants
|
|
// to refer to them instead of having to type them in by hand.
|
|
// Output is *appended* to outstring.
|
|
// ----------------------------------------------------------------------
|
|
|
|
class HeaderEntryStringHash { // not all STL implementations define this...
|
|
public:
|
|
hash<const char *> hash_; // ...but they all seem to define this
|
|
HeaderEntryStringHash() : hash_() {}
|
|
size_t operator()(const string& s) const {
|
|
return hash_(s.c_str()); // just convert the string to a const char*
|
|
}
|
|
};
|
|
|
|
static void WriteOneHeaderEntry(string *outstring,
|
|
const string& variable,
|
|
const string& full_pathname) {
|
|
LOCK(&g_header_mutex);
|
|
|
|
// we use hash_map instead of hash_set just to keep the stl size down
|
|
static hash_map<string, bool, HeaderEntryStringHash> vars_seen;
|
|
static string current_file;
|
|
static string prefix;
|
|
|
|
if (full_pathname != current_file) {
|
|
// changed files so re-initialize the static variables
|
|
vars_seen.clear();
|
|
current_file = full_pathname;
|
|
|
|
// remove the path before the filename
|
|
string filename;
|
|
string::size_type pos = full_pathname.rfind('/');
|
|
if (pos == string::npos) {
|
|
filename = full_pathname;
|
|
} else {
|
|
filename = full_pathname.substr(pos + 1);
|
|
}
|
|
|
|
prefix = "k";
|
|
bool take_next = true;
|
|
|
|
for (int i=0; i < filename.length(); i++) {
|
|
if (filename[i] == '.') {
|
|
// stop when we find the dot
|
|
break;
|
|
}
|
|
if (take_next) {
|
|
if (filename.substr(i,4) == "post") {
|
|
// stop before we process post...
|
|
break;
|
|
}
|
|
prefix = prefix + filename[i];
|
|
take_next = false;
|
|
}
|
|
if (filename[i] == '_') {
|
|
take_next = true;
|
|
}
|
|
}
|
|
prefix = prefix + "_";
|
|
}
|
|
|
|
// print out the variable, but only if we haven't seen it before.
|
|
if (vars_seen.find(variable) == vars_seen.end()) {
|
|
if (variable == kMainSectionName || variable.find("BI_") == 0) {
|
|
// We don't want to write entries for __MAIN__ or the built-ins
|
|
} else {
|
|
*outstring += (string("const char * const ")
|
|
+ prefix
|
|
+ variable
|
|
+ " = \""
|
|
+ variable
|
|
+ "\";\n");
|
|
}
|
|
vars_seen[variable] = true;
|
|
}
|
|
UNLOCK(&g_header_mutex);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Token
|
|
// A Token is a string marked with a token type enum. The string
|
|
// has different meanings for different token types. For text, the
|
|
// string is the text itself. For variable and template types, the
|
|
// string is the name of the variable holding the value or the
|
|
// template name, resp. For section types, the string is the name
|
|
// of the section, used to retrieve the hidden/visible state and
|
|
// the associated list of dictionaries, if any.
|
|
// ----------------------------------------------------------------------
|
|
|
|
enum TemplateTokenType { TOKENTYPE_UNUSED, TOKENTYPE_TEXT,
|
|
TOKENTYPE_VARIABLE, TOKENTYPE_SECTION_START,
|
|
TOKENTYPE_SECTION_END, TOKENTYPE_TEMPLATE,
|
|
TOKENTYPE_COMMENT, TOKENTYPE_NULL };
|
|
|
|
// A Token is a typed string. The semantics of the string depends on the
|
|
// token type, as follows:
|
|
// TOKENTYPE_TEXT - the text
|
|
// TOKENTYPE_VARIABLE - the name of the variable
|
|
// TOKENTYPE_SECTION_START - the name of the section being started
|
|
// TOKENTYPE_SECTION_END - the name of the section being ended
|
|
// TOKENTYPE_TEMPLATE - the name of the variable whose value will be
|
|
// the template filename
|
|
// TOKENTYPE_COMMENT - the empty string, not used
|
|
// TOKENTYPE_NULL - the empty string
|
|
// All non-comment tokens may also have modifiers, which follow the name
|
|
// of the token: the syntax is {{<PREFIX><NAME>:<mod>:<mod>:<mod>...}}
|
|
// The modifiers are also stored as a string, starting with the first :
|
|
struct Token {
|
|
TemplateTokenType type;
|
|
const char* text;
|
|
int textlen;
|
|
ModifierAndNonces modifier_plus_values;
|
|
Token(TemplateTokenType t, const char* txt, int len,
|
|
const ModifierAndNonces* modval)
|
|
: type(t), text(txt), textlen(len) {
|
|
if (modval) modifier_plus_values = *modval;
|
|
}
|
|
};
|
|
|
|
// This applies the modifiers to a string, modifying the string in place
|
|
static void ModifyString(const ModifierAndNonces& modifiers, string* s) {
|
|
for (ModifierAndNonces::const_iterator it = modifiers.begin();
|
|
it != modifiers.end(); ++it) {
|
|
*s = (*it->first)(*s, it->second);
|
|
}
|
|
}
|
|
|
|
}; // anonymous namespace
|
|
|
|
// ----------------------------------------------------------------------
|
|
// TemplateNode
|
|
// When we read a template, we decompose it into its components:
|
|
// variables, sections, include-templates, and runs of raw text.
|
|
// Each of these we see becomes one TemplateNode. TemplateNode
|
|
// is the abstract base class; each component has its own type.
|
|
// ----------------------------------------------------------------------
|
|
|
|
// This class allows us to support various types for Expand.
|
|
class ExpandEmitter {
|
|
public:
|
|
ExpandEmitter() {}
|
|
virtual ~ExpandEmitter() {}
|
|
virtual void Emit(const string& s) = 0;
|
|
virtual void Emit(const char* s) = 0;
|
|
virtual void Emit(const char* s, int slen) = 0;
|
|
};
|
|
|
|
class StringEmitter : public ExpandEmitter {
|
|
string* const outbuf_;
|
|
public:
|
|
StringEmitter(string* outbuf) : outbuf_(outbuf) {}
|
|
virtual void Emit(const string& s) { *outbuf_ += s; }
|
|
virtual void Emit(const char* s) { *outbuf_ += s; }
|
|
virtual void Emit(const char* s, int slen) { outbuf_->append(s, slen); }
|
|
};
|
|
|
|
class TemplateNode {
|
|
public:
|
|
TemplateNode() {}
|
|
virtual ~TemplateNode() {}
|
|
|
|
// Expands the template node using the supplied dictionary. The
|
|
// result is placed into output_buffer. If force_annotate_dictionary is
|
|
// not NULL, and force_annotate_dictionary->ShoudlAnnotateOutput() is
|
|
// true, the output is annotated, even if
|
|
// dictionary->ShouldAnnotateOutput() is false.
|
|
virtual void Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate) const = 0;
|
|
|
|
// Writes entries to a header file to provide syntax checking at
|
|
// compile time.
|
|
virtual void WriteHeaderEntries(string *outstring,
|
|
const string& filename) const = 0;
|
|
|
|
// Dumps a representation of the node and its subnodes to LOG(INFO)
|
|
// as a debugging aid.
|
|
virtual void Dump(int level) const = 0;
|
|
|
|
public:
|
|
// In addition to the pure-virtual API (above), we also define some
|
|
// useful helper functions, as static methods. These could have
|
|
// been global methods, but what the hey, we'll put them here.
|
|
|
|
// Returns the dictionary that thinks we should annotate, or NULL
|
|
// if nobody does. That is, returns dict if dict->ShouldAnnotateOutput()
|
|
// is true, force_annotate if force_annotate->ShouldAnnotateOutput() is
|
|
// true, and NULL else. force_annotate is usually a dict's parent
|
|
// dictionary, and is used with child nodes to annotate when their
|
|
// parents do. It can be NULL.
|
|
static const TemplateDictionary* ShouldAnnotateOutput(
|
|
const TemplateDictionary* dict,
|
|
const TemplateDictionary* force_annotate) {
|
|
if (dict->ShouldAnnotateOutput())
|
|
return dict;
|
|
if (force_annotate && force_annotate->ShouldAnnotateOutput())
|
|
return force_annotate;
|
|
return NULL;
|
|
}
|
|
|
|
// Return an opening template annotation, that can be emitted
|
|
// in the Expanded template output. Used for generating template
|
|
// debugging information in the result document.
|
|
static string OpenAnnotation(const string& name,
|
|
const string& value) {
|
|
// The opening annotation looks like: {{#name=value}}
|
|
return string("{{#") + name + string("=") + value + string("}}");
|
|
}
|
|
|
|
// Return a closing template annotation, that can be emitted
|
|
// in the Expanded template output. Used for generating template
|
|
// debugging information in the result document.
|
|
static string CloseAnnotation(const string& name) {
|
|
// The closing annotation looks like: {{/name}}
|
|
return string("{{/") + name + string("}}");
|
|
}
|
|
|
|
protected:
|
|
typedef list<TemplateNode *> NodeList;
|
|
|
|
private:
|
|
TemplateNode(const TemplateNode&); // disallow copying
|
|
void operator=(const TemplateNode&);
|
|
};
|
|
|
|
// ----------------------------------------------------------------------
|
|
// TextTemplateNode
|
|
// The simplest template-node: it holds runs of raw template text,
|
|
// that should be emitted verbatim. The text points into
|
|
// template_text_.
|
|
// ----------------------------------------------------------------------
|
|
|
|
class TextTemplateNode : public TemplateNode {
|
|
public:
|
|
explicit TextTemplateNode(const char* text, int textlen)
|
|
: text_(text), textlen_(textlen) {
|
|
VLOG(2) << "Constructing TextTemplateNode: " << string(text_, textlen_)
|
|
<< endl;
|
|
}
|
|
virtual ~TextTemplateNode() {
|
|
VLOG(2) << "Deleting TextTemplateNode: " << string(text_, textlen_)
|
|
<< endl;
|
|
}
|
|
|
|
// Expands the text node by simply outputting the text string. This
|
|
// virtual method does not use TemplateDictionary or force_annotate.
|
|
virtual void Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *,
|
|
const TemplateDictionary *) const {
|
|
output_buffer->Emit(text_, textlen_);
|
|
}
|
|
|
|
// A noop for text nodes
|
|
virtual void WriteHeaderEntries(string *outstring,
|
|
const string& filename) const {
|
|
return;
|
|
}
|
|
|
|
// Dumps a representation of the text node.
|
|
virtual void Dump(int level) const {
|
|
LOG(INFO) << std::setfill(' ') << std::setw(kIndent*level) << " "
|
|
<< "Text Node: -->|" << string(text_, textlen_) << "|<--"
|
|
<< endl;
|
|
}
|
|
|
|
private:
|
|
const char* text_; // The text held by this node
|
|
int textlen_;
|
|
};
|
|
|
|
// ----------------------------------------------------------------------
|
|
// VariableTemplateNode
|
|
// Holds a variable to be replaced when the template is expanded.
|
|
// The variable is stored in a token object, which has a char*
|
|
// that points into template_text_. There may also be modifiers,
|
|
// which are applied at Expand time.
|
|
// ----------------------------------------------------------------------
|
|
|
|
class VariableTemplateNode : public TemplateNode {
|
|
public:
|
|
explicit VariableTemplateNode(const Token& token)
|
|
: token_(token) {
|
|
VLOG(2) << "Constructing VariableTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
virtual ~VariableTemplateNode() {
|
|
VLOG(2) << "Deleting VariableTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
|
|
// Expands the variable node by outputting the value (if there is one)
|
|
// of the node variable which is retrieved from the dictionary
|
|
virtual void Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate) const;
|
|
|
|
virtual void WriteHeaderEntries(string *outstring,
|
|
const string& filename) const {
|
|
WriteOneHeaderEntry(outstring, string(token_.text, token_.textlen),
|
|
filename);
|
|
}
|
|
|
|
virtual void Dump(int level) const {
|
|
LOG(INFO) << std::setfill(' ') << std::setw(kIndent*level) << " "
|
|
<< "Variable Node: " << string(token_.text, token_.textlen)
|
|
<< endl;
|
|
}
|
|
|
|
protected:
|
|
const Token token_;
|
|
};
|
|
|
|
void VariableTemplateNode::Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate)
|
|
const {
|
|
string var(token_.text, token_.textlen);
|
|
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
// TODO(csilvers): include modifier info in the annotation
|
|
output_buffer->Emit(OpenAnnotation("VAR", var));
|
|
}
|
|
|
|
const char *value = dictionary->GetSectionValue(var);
|
|
if (token_.modifier_plus_values.empty()) { // no need to modify value
|
|
output_buffer->Emit(value); // so just emit it
|
|
} else {
|
|
string modified_value(value);
|
|
ModifyString(token_.modifier_plus_values, &modified_value);
|
|
output_buffer->Emit(modified_value);
|
|
}
|
|
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
output_buffer->Emit(CloseAnnotation("VAR"));
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// TemplateTemplateNode
|
|
// Holds a variable to be replaced by an expanded (included)
|
|
// template whose filename is the value of the variable in the
|
|
// dictionary.
|
|
// ----------------------------------------------------------------------
|
|
|
|
class TemplateTemplateNode : public TemplateNode {
|
|
public:
|
|
explicit TemplateTemplateNode(const Token& token, Strip strip)
|
|
: token_(token), strip_(strip) {
|
|
VLOG(2) << "Constructing TemplateTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
virtual ~TemplateTemplateNode() {
|
|
VLOG(2) << "Deleting TemplateTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
|
|
// Expands the template node by retrieving the name of a template
|
|
// file from the supplied dictionary, expanding it (using this
|
|
// dictionary if none other is provided in the TemplateDictionary),
|
|
// and then outputting this newly expanded template in place of the
|
|
// original variable.
|
|
virtual void Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate) const;
|
|
|
|
virtual void WriteHeaderEntries(string *outstring,
|
|
const string& filename) const {
|
|
WriteOneHeaderEntry(outstring, string(token_.text, token_.textlen),
|
|
filename);
|
|
}
|
|
|
|
virtual void Dump(int level) const {
|
|
LOG(INFO) << std::setfill(' ') << std::setw(kIndent*level) << " "
|
|
<< "Template Node: " << string(token_.text, token_.textlen)
|
|
<< endl;
|
|
}
|
|
|
|
protected:
|
|
const Token token_; // text is the name of a template file
|
|
Strip strip_; // Flag to pass from parent template to included template
|
|
};
|
|
|
|
// If no value is found in the dictionary for the template variable
|
|
// in this node, then no output is generated in place of this variable.
|
|
void TemplateTemplateNode::Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate)
|
|
const {
|
|
string variable(token_.text, token_.textlen);
|
|
if (dictionary->IsHiddenTemplate(variable)) {
|
|
// if this "template include" section is "hidden", do nothing
|
|
return;
|
|
}
|
|
|
|
// see if there is a vector of dictionaries for this template
|
|
const vector<TemplateDictionary*> *dv =
|
|
&dictionary->GetTemplateDictionaries(variable);
|
|
if (dv->empty()) // empty dict means 'expand once using containing dict'
|
|
dv = g_use_current_dict; // a vector with one element: NULL
|
|
|
|
vector<TemplateDictionary*>::const_iterator dv_iter = dv->begin();
|
|
for (; dv_iter != dv->end(); ++dv_iter) {
|
|
// We do this in the loop, because maybe one day we'll support
|
|
// each expansion having its own template dictionary. That's also
|
|
// why we pass in the dictionary-index as an argument.
|
|
const char* const filename = dictionary->GetIncludeTemplateName(
|
|
variable, dv_iter - dv->begin());
|
|
// if it wasn't set then treat it as if it were "hidden", i.e, do nothing
|
|
if (!filename || filename[0] == '\0')
|
|
continue;
|
|
|
|
// pass the flag values from the parent template to the included template
|
|
Template *included_template = Template::GetTemplate(filename, strip_);
|
|
|
|
// if there was a problem retrieving the template, bail!
|
|
if (!included_template) {
|
|
LOG(ERROR) << "Failed to load included template: " << filename << endl;
|
|
continue;
|
|
}
|
|
|
|
// Expand the included template once for each "template specific"
|
|
// dictionary. Normally this will only iterate once, but it's
|
|
// possible to supply a list of more than one sub-dictionary and
|
|
// then the template explansion will be iterative, just as though
|
|
// the included template were an iterated section.
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
output_buffer->Emit(OpenAnnotation("INC", variable));
|
|
}
|
|
|
|
// sub-dictionary NULL means 'just use the current dictionary instead'.
|
|
// We force children to annotate the output if we have to.
|
|
// If the include-template has modifiers, we need to expand to a string,
|
|
// modify the string, and append to output_buffer. Otherwise (common
|
|
// case), we can just expand into the output-buffer directly.
|
|
if (token_.modifier_plus_values.empty()) { // no need to modify sub-template
|
|
included_template->Expand(output_buffer,
|
|
*dv_iter ? *dv_iter : dictionary,
|
|
ShouldAnnotateOutput(dictionary, force_annotate));
|
|
} else {
|
|
string sub_template;
|
|
StringEmitter subtemplate_buffer(&sub_template);
|
|
included_template->Expand(&subtemplate_buffer,
|
|
*dv_iter ? *dv_iter : dictionary,
|
|
ShouldAnnotateOutput(dictionary, force_annotate));
|
|
ModifyString(token_.modifier_plus_values, &sub_template);
|
|
output_buffer->Emit(sub_template);
|
|
}
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
output_buffer->Emit(CloseAnnotation("INC"));
|
|
}
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// SectionTemplateNode
|
|
// Holds the name of a section and a list of subnodes contained
|
|
// in that section.
|
|
// ----------------------------------------------------------------------
|
|
|
|
class SectionTemplateNode : public TemplateNode {
|
|
public:
|
|
explicit SectionTemplateNode(const Token& token);
|
|
virtual ~SectionTemplateNode();
|
|
|
|
// The highest level parsing method. Reads a single token from the
|
|
// input -- taken from my_template->parse_state_ -- and adds the
|
|
// corresponding type of node to the template's parse
|
|
// tree. It may add a node of any type, whether text, variable,
|
|
// section, or template to the list of nodes contained in this
|
|
// section. Returns true iff we really added a node and didn't just
|
|
// end a section or hit a syntax error in the template file.
|
|
bool AddSubnode(Template *my_template);
|
|
|
|
// Expands a section node as follows:
|
|
// - Checks to see if the section is hidden and if so, does nothing but
|
|
// return
|
|
// - Tries to retrieve a list of dictionaries from the supplied dictionary
|
|
// stored under this section's name
|
|
// - If it finds a non-empty list of dictionaries, it iterates over the
|
|
// list and calls itself recursively to expand the section once for
|
|
// each dictionary
|
|
// - If there is no dictionary list (or an empty dictionary list somehow)
|
|
// is found, then the section is expanded once using the supplied
|
|
// dictionary. (This is the mechanism used to expand each single
|
|
// iteration of the section as well as to show a non-hidden section,
|
|
// allowing the section template syntax to be used for both conditional
|
|
// and iterative text).
|
|
virtual void Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary* force_annotate) const;
|
|
|
|
// Writes a header entry for the section name and calls the same
|
|
// method on all the nodes in the section
|
|
virtual void WriteHeaderEntries(string *outstring,
|
|
const string& filename) const;
|
|
|
|
virtual void Dump(int level) const;
|
|
|
|
protected:
|
|
const Token token_; // text is the name of the section
|
|
NodeList node_list_; // The list of subnodes in the section
|
|
|
|
// A protected method used in parsing the template file
|
|
// Finds the next token in the file and return it. Anything not inside
|
|
// a template marker is just text. Each template marker type, delimited
|
|
// by "{{" and "}}" is a different type of token. The first character
|
|
// inside the opening curly braces indicates the type of the marker,
|
|
// as follows:
|
|
// # - Start a section
|
|
// / - End a section
|
|
// > - A template file variable (the "include" directive)
|
|
// ! - A template comment
|
|
// <alnum or _> - A scalar variable
|
|
// One more thing. Before a name token is returned, if it happens to be
|
|
// any type other than a scalar variable, and if the next character after
|
|
// the closing curly braces is a newline, then the newline is eliminated
|
|
// from the output. This reduces the number of extraneous blank
|
|
// lines in the output. If the template author desires a newline to be
|
|
// retained after a final marker on a line, they must add a space character
|
|
// between the marker and the linefeed character.
|
|
Token GetNextToken(Template* my_template);
|
|
|
|
// The specific methods called used by AddSubnode to add the
|
|
// different types of nodes to this section node.
|
|
void AddTextNode(const char* text, int textlen);
|
|
void AddVariableNode(const Token& token);
|
|
void AddTemplateNode(const Token& token, Template* my_template);
|
|
void AddSectionNode(const Token& token, Template* my_template);
|
|
};
|
|
|
|
// --- constructor and destructor, Expand, Dump, and WriteHeaderEntries
|
|
|
|
SectionTemplateNode::SectionTemplateNode(const Token& token) : token_(token) {
|
|
VLOG(2) << "Constructing SectionTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
|
|
SectionTemplateNode::~SectionTemplateNode() {
|
|
VLOG(2) << "Deleting SectionTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << " and its subnodes"
|
|
<< endl;
|
|
|
|
// Need to delete the member of the list because the list is a list
|
|
// of pointers to these instances.
|
|
NodeList::iterator iter = node_list_.begin();
|
|
for (; iter != node_list_.end(); ++iter) {
|
|
delete (*iter);
|
|
}
|
|
VLOG(2) << "Finished deleting subnodes of SectionTemplateNode: "
|
|
<< string(token_.text, token_.textlen) << endl;
|
|
}
|
|
|
|
void SectionTemplateNode::Expand(ExpandEmitter *output_buffer,
|
|
const TemplateDictionary *dictionary,
|
|
const TemplateDictionary *force_annotate)
|
|
const {
|
|
const vector<TemplateDictionary*> *dv;
|
|
|
|
string variable(token_.text, token_.textlen);
|
|
|
|
// The section named __MAIN__ is special: you always expand it exactly
|
|
// once using the containing (main) dictionary.
|
|
if (token_.text == kMainSectionName) {
|
|
dv = g_use_current_dict; // 'expand once, using the passed in dictionary'
|
|
} else {
|
|
if (dictionary->IsHiddenSection(variable)) {
|
|
return; // if this section is "hidden", do nothing
|
|
}
|
|
dv = &dictionary->GetDictionaries(variable);
|
|
if (dv->empty()) // empty dict means 'expand once using containing dict'
|
|
dv = g_use_current_dict; // a vector with one element: NULL
|
|
}
|
|
|
|
vector<TemplateDictionary*>::const_iterator dv_iter = dv->begin();
|
|
for (; dv_iter != dv->end(); ++dv_iter) {
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
output_buffer->Emit(OpenAnnotation("SEC", variable));
|
|
}
|
|
|
|
// Expand using the section-specific dictionary. A sub-dictionary
|
|
// of NULL means 'just use the current dictionary instead'.
|
|
// We force children to annotate the output if we have to.
|
|
NodeList::const_iterator iter = node_list_.begin();
|
|
for (; iter != node_list_.end(); ++iter) {
|
|
(*iter)->Expand(output_buffer,
|
|
*dv_iter ? *dv_iter : dictionary,
|
|
ShouldAnnotateOutput(dictionary, force_annotate));
|
|
}
|
|
|
|
if (ShouldAnnotateOutput(dictionary, force_annotate)) {
|
|
output_buffer->Emit(CloseAnnotation("SEC"));
|
|
}
|
|
}
|
|
}
|
|
|
|
void SectionTemplateNode::WriteHeaderEntries(string *outstring,
|
|
const string& filename) const {
|
|
WriteOneHeaderEntry(outstring, string(token_.text, token_.textlen),
|
|
filename);
|
|
|
|
NodeList::const_iterator iter = node_list_.begin();
|
|
for (; iter != node_list_.end(); ++iter) {
|
|
(*iter)->WriteHeaderEntries(outstring, filename);
|
|
}
|
|
}
|
|
|
|
void SectionTemplateNode::Dump(int level) const {
|
|
LOG(INFO) << std::setfill(' ') << std::setw(kIndent*level) << " "
|
|
<< "Section Start: " << string(token_.text, token_.textlen)
|
|
<< endl;
|
|
NodeList::const_iterator iter = node_list_.begin();
|
|
for (; iter != node_list_.end(); ++iter) {
|
|
(*iter)->Dump(level+1);
|
|
}
|
|
LOG(INFO) << std::setfill(' ') << std::setw(kIndent*level) << " "
|
|
<< "Section End: " << string(token_.text, token_.textlen) << endl;
|
|
}
|
|
|
|
// --- AddSubnode and its sub-routines
|
|
|
|
void SectionTemplateNode::AddTextNode(const char* text, int textlen) {
|
|
if (text != "") { // ignore null text sections
|
|
node_list_.push_back(new TextTemplateNode(text, textlen));
|
|
}
|
|
}
|
|
|
|
void SectionTemplateNode::AddVariableNode(const Token& token) {
|
|
node_list_.push_back(new VariableTemplateNode(token));
|
|
}
|
|
|
|
// AddSectionNode
|
|
void SectionTemplateNode::AddSectionNode(const Token& token,
|
|
Template *my_template) {
|
|
SectionTemplateNode *new_node = new SectionTemplateNode(token);
|
|
|
|
// Not only create a new section node, but fill it with all *its*
|
|
// subnodes by repeatedly calling AddSubNode until it returns false
|
|
// (indicating either the end of the section or a syntax error)
|
|
while (new_node->AddSubnode(my_template)) {
|
|
// Found a new subnode to add
|
|
}
|
|
node_list_.push_back(new_node);
|
|
}
|
|
|
|
void SectionTemplateNode::AddTemplateNode(const Token& token,
|
|
Template *my_template) {
|
|
// pass the flag values from my_template to the new node
|
|
node_list_.push_back(new TemplateTemplateNode(token, my_template->strip_));
|
|
}
|
|
|
|
bool SectionTemplateNode::AddSubnode(Template *my_template) {
|
|
// Don't proceed if we already found an error
|
|
if (my_template->state() == TS_ERROR) {
|
|
return false;
|
|
}
|
|
|
|
// Stop when the buffer is empty.
|
|
if (my_template->parse_state_.bufstart >= my_template->parse_state_.bufend) {
|
|
// running out of file contents ends the section too
|
|
if (token_.text != kMainSectionName) {
|
|
// if we are not in the main section, we have a syntax error in the file
|
|
LOG_TEMPLATE_NAME(ERROR, my_template);
|
|
LOG(ERROR) << "File ended before all sections were closed" << endl;
|
|
my_template->set_state(TS_ERROR);
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Token token = GetNextToken(my_template);
|
|
|
|
switch (token.type) {
|
|
case TOKENTYPE_TEXT:
|
|
this->AddTextNode(token.text, token.textlen);
|
|
break;
|
|
case TOKENTYPE_VARIABLE:
|
|
this->AddVariableNode(token);
|
|
break;
|
|
case TOKENTYPE_SECTION_START:
|
|
this->AddSectionNode(token, my_template);
|
|
break;
|
|
case TOKENTYPE_SECTION_END:
|
|
// Don't add a node. Just make sure we are ending the right section
|
|
// and return false to indicate the section is complete
|
|
if (token.textlen != token_.textlen ||
|
|
memcmp(token.text, token_.text, token.textlen)) {
|
|
LOG_TEMPLATE_NAME(ERROR, my_template);
|
|
LOG(ERROR) << "Found end of different section than the one I am in"
|
|
<< "\nFound: " << string(token.text, token.textlen)
|
|
<< "\nIn: " << string(token_.text, token_.textlen) << endl;
|
|
my_template->set_state(TS_ERROR);
|
|
}
|
|
return false;
|
|
break;
|
|
case TOKENTYPE_TEMPLATE:
|
|
this->AddTemplateNode(token, my_template);
|
|
break;
|
|
case TOKENTYPE_COMMENT:
|
|
// Do nothing. Comments just drop out of the file altogether.
|
|
break;
|
|
case TOKENTYPE_NULL:
|
|
// GetNextToken either hit the end of the file or a syntax error
|
|
// in the file. Do nothing more here. Just return false to stop
|
|
// processing.
|
|
return false;
|
|
break;
|
|
default:
|
|
// This shouldn't happen. If it does, it's a programmer error.
|
|
LOG_TEMPLATE_NAME(ERROR, my_template);
|
|
LOG(ERROR) << "Invalid token type returned from GetNextToken" << endl;
|
|
}
|
|
// for all the cases where we did not return false
|
|
return true;
|
|
}
|
|
|
|
// --- GetNextToken and its subroutines
|
|
|
|
// A valid marker name is made up of alphanumerics and underscores...
|
|
// nothing else.
|
|
static bool IsValidName(const char* name, int namelen) {
|
|
for (const char *cur_char = name; cur_char - name < namelen; ++cur_char) {
|
|
if (!isalnum(*cur_char) && *cur_char != '_')
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// If we're pointing to the end of a line, and in a high enough strip mode,
|
|
// pass over the newline. If the line ends in a \, we skip over the \ and
|
|
// keep the newline. Returns a pointer to the new 'start' location, which
|
|
// is either 'start' or after a newline.
|
|
static const char* MaybeEatNewline(const char* start, const char* end,
|
|
Strip strip) {
|
|
// first, see if we have the escaped linefeed sequence
|
|
if (end - start >= 2 && start[0] == '\\' && start[1] == '\n') {
|
|
++start; // skip over the \, which keeps the \n
|
|
} else if (end - start >= 1 && start[0] == '\n' &&
|
|
strip >= STRIP_WHITESPACE) {
|
|
++start; // skip over the \n in high strip_ modes
|
|
}
|
|
return start;
|
|
}
|
|
|
|
// The root directory of templates
|
|
string *Template::template_root_directory_ = NULL;
|
|
|
|
// When the parse fails, we take several actions. msg is a stream
|
|
#define FAIL(msg) do { \
|
|
LOG_TEMPLATE_NAME(ERROR, my_template); \
|
|
LOG(ERROR) << msg << endl; \
|
|
my_template->set_state(TS_ERROR); \
|
|
/* make extra-sure we never try to parse anything more */ \
|
|
my_template->parse_state_.bufstart = my_template->parse_state_.bufend; \
|
|
return Token(TOKENTYPE_NULL, "", 0, NULL); \
|
|
} while (0)
|
|
|
|
// Parses the text of the template file in the input_buffer as
|
|
// follows: If the buffer is empty, return the null token. If getting
|
|
// text, search for the next "{{" sequence. If one is found, return
|
|
// all the text collected up to that sequence in a TextToken and
|
|
// change the token-parsing phase variable to GETTING_NAME, so the next
|
|
// call will know to look for a named marker, instead of more text.
|
|
// If getting a name, read the next character to learn what kind of
|
|
// marker it is. Then collect the characters of the name up to the
|
|
// "}}" sequence. If the "name" is a template comment, then we do not
|
|
// return the text of the comment in the token. If it is any other
|
|
// valid type of name, we return the token with the appropriate type
|
|
// and the name. If any syntax errors are discovered (like
|
|
// inappropriate characters in a name, not finding the closing curly
|
|
// braces, etc.) an error message is logged, the error state of the
|
|
// template is set, and a NULL token is returned. Updates parse_state_.
|
|
Token SectionTemplateNode::GetNextToken(Template *my_template) {
|
|
Template::ParseState* ps = &my_template->parse_state_; // short abbrev.
|
|
const char* token_start = ps->bufstart;
|
|
|
|
if (ps->bufstart >= ps->bufend) { // at end of buffer
|
|
return Token(TOKENTYPE_NULL, "", 0, NULL);
|
|
}
|
|
|
|
switch (ps->phase) {
|
|
case Template::ParseState::GETTING_TEXT: {
|
|
const char* token_end = (const char*)memchr(ps->bufstart, '{',
|
|
ps->bufend - ps->bufstart);
|
|
if (!token_end) {
|
|
// Didn't find a '{' so just grab all the rest of the buffer
|
|
token_end = ps->bufend;
|
|
ps->bufstart = ps->bufend; // next token will start at EOF
|
|
} else {
|
|
// see if the next char is also a '{' and remove it if it is
|
|
// ...but NOT if the next TWO characters are "{{"
|
|
if ((token_end+1 < ps->bufend && token_end[1] == '{') &&
|
|
!(token_end+2 < ps->bufend && token_end[2] == '{')) {
|
|
// Next is a {{. Eat that up and move to GETTING_NAME mode
|
|
ps->phase = Template::ParseState::GETTING_NAME;
|
|
ps->bufstart = token_end+2; // next token will start past {{
|
|
} else {
|
|
++token_end; // the { is part of our text we're reading
|
|
ps->bufstart = token_end; // next token starts just after the {
|
|
}
|
|
}
|
|
return Token(TOKENTYPE_TEXT, token_start, token_end-token_start, NULL);
|
|
}
|
|
|
|
case Template::ParseState::GETTING_NAME: {
|
|
TemplateTokenType ttype;
|
|
// Find out what type of name we are getting
|
|
switch (token_start[0]) {
|
|
case '#':
|
|
ttype = TOKENTYPE_SECTION_START;
|
|
++token_start;
|
|
break;
|
|
case '/':
|
|
ttype = TOKENTYPE_SECTION_END;
|
|
++token_start;
|
|
break;
|
|
case '!':
|
|
ttype = TOKENTYPE_COMMENT;
|
|
++token_start;
|
|
break;
|
|
case '>':
|
|
ttype = TOKENTYPE_TEMPLATE;
|
|
++token_start;
|
|
break;
|
|
default:
|
|
// the assumption that the next char is alnum or _ will be
|
|
// tested below in the call to IsValidName().
|
|
ttype = TOKENTYPE_VARIABLE;
|
|
}
|
|
|
|
// Now get the name (or the comment, as the case may be)
|
|
const char* token_end = (const char*)memchr(token_start, '}',
|
|
ps->bufend - token_start);
|
|
|
|
if (!token_end) { // Didn't find a '}', so name never ended. Error!
|
|
FAIL("No ending '}' when parsing name starting with '"
|
|
<< string(token_start, ps->bufend-token_start) << "'");
|
|
}
|
|
|
|
// We found a }. Next char should be a } too, or name isn't ended; error!
|
|
if (token_end+1 == ps->bufend || token_end[1] != '}') {
|
|
if (ttype == TOKENTYPE_COMMENT) {
|
|
FAIL("Illegal to use the '}' character in template "
|
|
<< "comment that starts with '"
|
|
<< string(token_start, token_end+1 - token_start) << "'");
|
|
} else {
|
|
FAIL("Invalid name in template starting with '"
|
|
<< string(token_start, token_end+1 - token_start) << "'");
|
|
}
|
|
}
|
|
|
|
// Comments are a special case, since they don't have a name or action
|
|
if (ttype == TOKENTYPE_COMMENT) {
|
|
ps->phase = Template::ParseState::GETTING_TEXT;
|
|
ps->bufstart = token_end + 2; // read past the }}
|
|
// If requested, remove any unescaped linefeed following a comment
|
|
ps->bufstart = MaybeEatNewline(ps->bufstart, ps->bufend,
|
|
my_template->strip_);
|
|
// For comments, don't bother returning the text
|
|
return Token(ttype, "", 0, NULL);
|
|
}
|
|
|
|
// Now we have the name, possibly with following modifiers.
|
|
// Find the modifier-start.
|
|
const char* mod_start = (const char*)memchr(token_start, ':',
|
|
token_end - token_start);
|
|
if (mod_start == NULL)
|
|
mod_start = token_end;
|
|
|
|
// Make sure the name is legal.
|
|
if (ttype != TOKENTYPE_COMMENT &&
|
|
!IsValidName(token_start, mod_start - token_start)) {
|
|
FAIL("Illegal name in template '"
|
|
<< string(token_start, mod_start-token_start) << "'");
|
|
}
|
|
|
|
// Figure out what all the modifiers are. Mods are colon-separated.
|
|
ModifierAndNonces modifiers;
|
|
const char* mod_end;
|
|
for (const char* mod = mod_start; mod < token_end; mod = mod_end) {
|
|
assert(*mod == ':');
|
|
++mod; // skip past the starting colon
|
|
mod_end = (const char*)memchr(mod, ':', token_end - mod);
|
|
if (mod_end == NULL)
|
|
mod_end = token_end;
|
|
// Modifiers can be of the form :modname=value. Extract out value
|
|
const char* value = (const char*)memchr(mod, '=', mod_end - mod);
|
|
if (value == NULL)
|
|
value = mod_end;
|
|
string value_string(value, mod_end - value);
|
|
// Convert the string to a functor, and error out if we can't.
|
|
int mod_index = -1;
|
|
for (int i = 0; i < sizeof(g_modifiers)/sizeof(*g_modifiers); ++i) {
|
|
if ((value - mod == 1 &&
|
|
*mod == g_modifiers[i].short_name) ||
|
|
(value - mod == strlen(g_modifiers[i].long_name) &&
|
|
!memcmp(mod, g_modifiers[i].long_name, value - mod))) {
|
|
mod_index = i;
|
|
break;
|
|
}
|
|
}
|
|
// There are various ways a modifier syntax can be illegal.
|
|
if (mod_index == -1) {
|
|
FAIL("Unknown modifier for variable "
|
|
<< string(token_start, mod_start - token_start) << ": "
|
|
<< "'" << string(mod, value - mod) << "'");
|
|
} else if ((g_modifiers[mod_index].value_status == MODVAL_FORBIDDEN &&
|
|
value < mod_end)) {
|
|
FAIL("Modifier for variable "
|
|
<< string(token_start, mod_start - token_start) << ":"
|
|
<< string(mod, value - mod) << " "
|
|
<< "has illegal mod-value '" << value_string << "'");
|
|
} else if ((g_modifiers[mod_index].value_status == MODVAL_REQUIRED &&
|
|
value == mod_end)) {
|
|
FAIL("Modifier for variable "
|
|
<< string(token_start, mod_start - token_start) << ":"
|
|
<< string(mod, value - mod) << " "
|
|
<< "is missing a required mod-value");
|
|
}
|
|
|
|
modifiers.push_back(pair<ModifierFunctor,string>(
|
|
g_modifiers[mod_index].modifier, value_string));
|
|
}
|
|
|
|
// For now, we only allow variable and include nodes to have modifiers.
|
|
// TODO(csilvers): figure out if it's useful to have to for sections
|
|
if (!modifiers.empty() &&
|
|
ttype != TOKENTYPE_VARIABLE && ttype != TOKENTYPE_TEMPLATE) {
|
|
FAIL(string(token_start, token_end - token_start)
|
|
<< "malformed: only variables and template-includes "
|
|
<< "are allowed to have modifiers");
|
|
}
|
|
|
|
// Whew! We passed the guantlet. Get ready for the next token
|
|
ps->phase = Template::ParseState::GETTING_TEXT;
|
|
ps->bufstart = token_end + 2; // read past the }}
|
|
// If requested, remove any linefeed following a comment,
|
|
// or section start or end, or template marker, unless
|
|
// it is escaped by '\'
|
|
if (ttype != TOKENTYPE_VARIABLE) {
|
|
ps->bufstart = MaybeEatNewline(ps->bufstart, ps->bufend,
|
|
my_template->strip_);
|
|
}
|
|
|
|
// create and return the TEXT token that we found
|
|
return Token(ttype, token_start, mod_start - token_start, &modifiers);
|
|
}
|
|
|
|
default: {
|
|
FAIL("Programming error: Unexpected parse phase while "
|
|
<< "parsing template: " << ps->phase);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::Template()
|
|
// Template::~Template()
|
|
// Template::AssureGlobalsInitialized()
|
|
// Template::GetTemplate()
|
|
// Calls ReloadIfChanged to load the template the first time.
|
|
// The constructor is private; GetTemplate() is the factory
|
|
// method used to actually construct a new template if needed.
|
|
// ----------------------------------------------------------------------
|
|
|
|
Template::Template(const string& filename, Strip strip)
|
|
: filename_(filename), filename_mtime_(0), strip_(strip),
|
|
state_(TS_EMPTY),
|
|
template_text_(NULL), template_text_len_(0), tree_(NULL),
|
|
parse_state_(), mutex_(new RWLock) {
|
|
// Make sure g_use_current_dict, etc. are initted before any possbility
|
|
// of calling Expand() or other Template classes that access globals.
|
|
AssureGlobalsInitialized();
|
|
|
|
// phase_ indicates what type of thing we expect next during tokenization.
|
|
// We start off expecting text, hence the initial value is GETTING_TEXT
|
|
|
|
VLOG(2) << endl << "Constructing Template for " << template_file() << endl;
|
|
|
|
// Preserve whitespace in Javascript files because carriage returns
|
|
// can convey meaning for comment termination and closures
|
|
if ( strip == STRIP_WHITESPACE && filename.length() >= 3 &&
|
|
!strcmp(filename.c_str() + filename.length() - 3, ".js") ) {
|
|
strip = STRIP_BLANK_LINES;
|
|
}
|
|
|
|
ReloadIfChanged();
|
|
}
|
|
|
|
Template::~Template() {
|
|
VLOG(2) << endl << "Deleting Template for " << template_file() << endl;
|
|
delete mutex_;
|
|
delete tree_;
|
|
// Delete this last, since tree has pointers into template_text_
|
|
delete[] template_text_;
|
|
}
|
|
|
|
// NOTE: This function must be called by any static function that
|
|
// accesses any of the variables set here.
|
|
void Template::AssureGlobalsInitialized() {
|
|
LOCK(&g_static_mutex); // protects all the vars defined here
|
|
if (template_root_directory_ == NULL) { // only need to run this once!
|
|
template_root_directory_ = new string(kDefaultTemplateDirectory);
|
|
// this_dict is a dictionary with a single NULL entry in it
|
|
g_use_current_dict = new vector<TemplateDictionary*>;
|
|
g_use_current_dict->push_back(NULL);
|
|
}
|
|
UNLOCK(&g_static_mutex);
|
|
}
|
|
|
|
Template *Template::GetTemplate(const string& filename, Strip strip) {
|
|
// No need to have the cache-mutex acquired for this step
|
|
string abspath(PathJoin(template_root_directory(), filename));
|
|
|
|
LOCK(&g_cache_mutex);
|
|
if (g_template_cache == NULL)
|
|
g_template_cache = new TemplateCache;
|
|
|
|
Template *tpl = (*g_template_cache)[pair<string, Strip>(abspath, strip)];
|
|
if (tpl) {
|
|
// Note: if the status is TS_ERROR here, we don't attempt
|
|
// to reload the template file, but we don't return
|
|
// the template object either
|
|
if (tpl->state() == TS_RELOAD) {
|
|
tpl->ReloadIfChanged();
|
|
}
|
|
} else {
|
|
tpl = new Template(abspath, strip);
|
|
(*g_template_cache)[pair<string, Strip>(abspath, strip)] = tpl;
|
|
}
|
|
UNLOCK(&g_cache_mutex); // we're done messing with g_template_cache
|
|
|
|
// if the statis is not TS_READY, then it is TS_ERROR at this
|
|
// point. If it is TS_ERROR, we leave the state as is, but return
|
|
// NULL. We won't try to load the template file again until the
|
|
// state gets changed to TS_RELOAD by another call to
|
|
// ReloadAllIfChanged.
|
|
if (tpl->state() != TS_READY) {
|
|
return NULL;
|
|
} else {
|
|
return tpl;
|
|
}
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::BuildTree()
|
|
// Template::WriteHeaderEntry()
|
|
// Template::Dump()
|
|
// These kick off their various parsers -- BuildTree for the
|
|
// main task of parsing a Template when it's read from memory,
|
|
// WriteHeaderEntry for parsing for make_tpl_varnames_h, and
|
|
// Dump() for when Dump() is called by the caller.
|
|
// ----------------------------------------------------------------------
|
|
|
|
// NOTE: BuildTree takes over ownership of input_buffer, and will delete it.
|
|
// It should have been created via new[].
|
|
bool Template::BuildTree(const char* input_buffer,
|
|
const char* input_buffer_end) {
|
|
// Assign an arbitrary name to the top-level node
|
|
parse_state_.bufstart = input_buffer;
|
|
parse_state_.bufend = input_buffer_end;
|
|
parse_state_.phase = ParseState::GETTING_TEXT;
|
|
SectionTemplateNode *top_node = new SectionTemplateNode(
|
|
Token(TOKENTYPE_SECTION_START,
|
|
kMainSectionName, strlen(kMainSectionName), NULL));
|
|
while (top_node->AddSubnode(this)) {
|
|
// Add the rest of the template in.
|
|
}
|
|
|
|
// get rid of the old tree, whenever we try to build a new one.
|
|
delete tree_;
|
|
delete[] template_text_;
|
|
tree_ = top_node;
|
|
template_text_ = input_buffer;
|
|
template_text_len_ = input_buffer_end - input_buffer;
|
|
|
|
if (state() != TS_ERROR) { // BuildTree had no problem parsing
|
|
set_state(TS_READY);
|
|
return true;
|
|
} else {
|
|
delete tree_;
|
|
tree_ = NULL;
|
|
delete[] template_text_;
|
|
template_text_ = NULL;
|
|
template_text_len_ = 0;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void Template::WriteHeaderEntries(string *outstring) const {
|
|
if (state() == TS_READY) { // only write header entries for 'good' tpls
|
|
tree_->WriteHeaderEntries(outstring, template_file());
|
|
}
|
|
}
|
|
|
|
void Template::Dump(const char *filename) const {
|
|
LOG(INFO) << "------------Start Template Dump ["
|
|
<< filename << "]--------------" << endl;
|
|
if (tree_) {
|
|
tree_->Dump(1);
|
|
} else {
|
|
LOG(INFO) << "No parse tree has been produced for this template" << endl;
|
|
}
|
|
LOG(INFO) << "------------End Template Dump----------------" << endl;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::SetTemplateRootDirectory()
|
|
// Template::template_root_directory()
|
|
// Template::state()
|
|
// Template::set_state()
|
|
// Template::template_file()
|
|
// Various introspection or functionality-modifier methods.
|
|
// The template-root-directory is where we look for template
|
|
// files (in GetTemplate and include templates) when they're
|
|
// given with a relative rather than absolute name. state()
|
|
// is the parse-state (success, error). template_file() is
|
|
// the filename of a given template object's input.
|
|
// ----------------------------------------------------------------------
|
|
|
|
// TODO(csilvers): convert input into an absolute path using getcwd() if
|
|
// needed? That makes code safer it client does a chdir().
|
|
bool Template::SetTemplateRootDirectory(const string& directory) {
|
|
// Make sure template_root_directory_ has been initialized
|
|
AssureGlobalsInitialized();
|
|
|
|
// This is needed since we access/modify template_root_directory_
|
|
LOCK(&g_static_mutex);
|
|
// make sure it ends with '/'
|
|
if (directory.length() == 0 || directory[directory.length()-1] != '/') {
|
|
*template_root_directory_ = directory + '/';
|
|
} else {
|
|
*template_root_directory_ = directory;
|
|
}
|
|
VLOG(2) << "Setting Template directory to " << *template_root_directory_
|
|
<< endl;
|
|
UNLOCK(&g_static_mutex);
|
|
return true;
|
|
}
|
|
|
|
// It's not safe to return a string& in threaded contexts
|
|
string Template::template_root_directory() {
|
|
// Make sure template_root_directory_ has been initialized
|
|
AssureGlobalsInitialized();
|
|
LOCK(&g_static_mutex); // protects the static var template_root_directory_
|
|
string retval = *template_root_directory_;
|
|
UNLOCK(&g_static_mutex);
|
|
return retval;
|
|
}
|
|
|
|
void Template::set_state(TemplateState new_state) {
|
|
state_ = new_state;
|
|
}
|
|
|
|
TemplateState Template::state() const {
|
|
return state_;
|
|
}
|
|
|
|
const char *Template::template_file() const {
|
|
return filename_.c_str();
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::ReloadIfChanged()
|
|
// Template::ReloadAllIfChanged()
|
|
// If one template, try immediately to reload it from disk. If
|
|
// all templates, just set all their statuses to TS_RELOAD, so
|
|
// next time GetTemplate() is called on the template, it will
|
|
// be reloaded from disk if the disk version is newer than the
|
|
// one currently in memory. ReloadIfChanged() returns true
|
|
// if the file changed and disk *and* we successfully reloaded
|
|
// and parsed it. It never returns true if filename_ is "".
|
|
// ----------------------------------------------------------------------
|
|
|
|
bool Template::ReloadIfChanged() {
|
|
if (filename_.empty()) return false;
|
|
|
|
// This entire routine is protected by mutex_ so when it's called
|
|
// from different threads, they don't stomp on tree_ and state_.
|
|
// This is still not perfect, since set_filename() could stomp
|
|
// on filename_ while we're reading it, but it's good enough.
|
|
mutex_->LockRW();
|
|
|
|
struct stat statbuf;
|
|
if (stat(filename_.c_str(), &statbuf) != 0) {
|
|
LOG(WARNING) << "Unable to stat file " << filename_ << endl;
|
|
// We keep the old tree if there is one, otherwise we're in error
|
|
set_state(tree_ ? TS_READY : TS_ERROR);
|
|
mutex_->Unlock();
|
|
return false;
|
|
}
|
|
if (statbuf.st_mtime == filename_mtime_ && filename_mtime_ > 0
|
|
&& tree_) { // force a reload if we don't already have a tree_
|
|
VLOG(1) << "Not reloading file " << filename_ << ": no new mod-time" << endl;
|
|
set_state(TS_READY);
|
|
mutex_->Unlock();
|
|
return false; // file's timestamp hasn't changed, so no need to reload
|
|
}
|
|
|
|
FILE* fp = fopen(filename_.c_str(), "r");
|
|
if (fp == NULL) {
|
|
LOG(ERROR) << "Can't find file " << filename_ << "; skipping" << endl;
|
|
// We keep the old tree if there is one, otherwise we're in error
|
|
set_state(tree_ ? TS_READY : TS_ERROR);
|
|
mutex_->Unlock();
|
|
return false;
|
|
}
|
|
char* file_buffer = new char[statbuf.st_size];
|
|
if ( fread(file_buffer, 1, statbuf.st_size, fp) != statbuf.st_size ) {
|
|
LOG(ERROR) << "Error reading file " << filename_
|
|
<< ": " << strerror(errno) << endl;
|
|
fclose(fp);
|
|
delete[] file_buffer;
|
|
// We could just keep the old tree, but probably safer to say 'error'
|
|
set_state(TS_ERROR);
|
|
mutex_->Unlock();
|
|
return false;
|
|
}
|
|
fclose(fp);
|
|
|
|
// Now that we know we've read the file ok, mark the new mtime
|
|
filename_mtime_ = statbuf.st_mtime;
|
|
|
|
// Parse the input one line at a time to get the "stripped" input.
|
|
// Note stripping only makes smaller, so st_size is a safe upper bound.
|
|
char* input_buffer = new char[statbuf.st_size];
|
|
const int buflen = InsertFile(file_buffer, statbuf.st_size, input_buffer);
|
|
delete[] file_buffer;
|
|
|
|
// Now parse the template we just read. BuildTree takes over ownership
|
|
// of input_buffer in every case, and will eventually delete it.
|
|
if ( BuildTree(input_buffer, input_buffer + buflen) ) {
|
|
assert(state() == TS_READY);
|
|
mutex_->Unlock();
|
|
return true;
|
|
} else {
|
|
assert(state() != TS_READY);
|
|
mutex_->Unlock();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void Template::ReloadAllIfChanged() {
|
|
LOCK(&g_cache_mutex); // this protects the static g_template_cache
|
|
if (g_template_cache == NULL) {
|
|
UNLOCK(&g_cache_mutex);
|
|
return;
|
|
}
|
|
for (TemplateCache::const_iterator iter = g_template_cache->begin();
|
|
iter != g_template_cache->end();
|
|
++iter) {
|
|
(*iter).second->set_state(TS_RELOAD);
|
|
}
|
|
UNLOCK(&g_cache_mutex);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::ClearCache()
|
|
// Deletes all the objects in the template cache
|
|
// ----------------------------------------------------------------------
|
|
|
|
void Template::ClearCache() {
|
|
LOCK(&g_cache_mutex); // this protects the static g_template_cache
|
|
if (g_template_cache == NULL) {
|
|
UNLOCK(&g_cache_mutex);
|
|
return;
|
|
}
|
|
for (TemplateCache::const_iterator iter = g_template_cache->begin();
|
|
iter != g_template_cache->end();
|
|
++iter) {
|
|
delete (*iter).second;
|
|
}
|
|
delete g_template_cache;
|
|
g_template_cache = NULL;
|
|
UNLOCK(&g_cache_mutex);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// IsBlankOrOnlyHasOneRemovableMarker()
|
|
// Template::InsertLine()
|
|
// Template::InsertFile()
|
|
// InsertLine() is called by ReloadIfChanged for each line of
|
|
// the input. In general we just add it to a char buffer so we can
|
|
// parse the entire file at once in a slurp. However, we do one
|
|
// per-line check: see if the line is either all white space or has
|
|
// exactly one "removable" marker on it and nothing else. A marker
|
|
// is "removable" if it is either a comment, a start section, an
|
|
// end section, or a template include marker. This affects whether
|
|
// we add a newline or not, in certain Strip modes.
|
|
// InsertFile() takes an entire file in, as a string, and calls
|
|
// InsertLine() on each line, building up the output buffer line
|
|
// by line.
|
|
// Both functions require an output buffer big enough to hold
|
|
// the potential output (for InsertFile(), the output is guaranteed
|
|
// to be no bigger than the input). They both return the number
|
|
// of bytes they wrote to the output buffer.
|
|
// ----------------------------------------------------------------------
|
|
|
|
static void StripWhiteSpace(const char** str, int* len) {
|
|
// strip off trailing whitespace
|
|
while ((*len) > 0 && isspace((*str)[(*len)-1])) {
|
|
(*len)--;
|
|
}
|
|
|
|
// strip off leading whitespace
|
|
while ((*len) > 0 && isspace((*str)[0])) {
|
|
(*len)--;
|
|
(*str)++;
|
|
}
|
|
}
|
|
|
|
// Adjusts line and length iff condition is met, and RETURNS true.
|
|
static bool IsBlankOrOnlyHasOneRemovableMarker(const char** line, int* len) {
|
|
const char *clean_line = *line;
|
|
int new_len = *len;
|
|
StripWhiteSpace(&clean_line, &new_len);
|
|
|
|
// If there was only white space on the line, new_len will now be zero.
|
|
// In that case the line should be removed, so return true.
|
|
if (new_len == 0) {
|
|
(*line) = clean_line;
|
|
(*len) = new_len;
|
|
return true;
|
|
}
|
|
|
|
// The smallest removable marker is {{!}}, which requires five characters.
|
|
// If there aren't enough characters, then keep the line by returning false.
|
|
if (new_len < 5) {
|
|
return false;
|
|
}
|
|
|
|
if (clean_line[0] != '{' // if first two chars are not "{{"
|
|
|| clean_line[1] != '{'
|
|
|| (clean_line[2] != '#' // or next char marks neither section start
|
|
&& clean_line[2] != '/' // nor section end
|
|
&& clean_line[2] != '>' // nor template include
|
|
&& clean_line[2] != '!')) { // nor comment
|
|
return false; // then not what we are looking for.
|
|
}
|
|
|
|
const char *end_marker = strstr(clean_line, "}}");
|
|
|
|
if (!end_marker // if didn't find "}}"
|
|
|| end_marker != &clean_line[new_len - 2]) { // or it wasn't at the end
|
|
return false;
|
|
}
|
|
|
|
// else return the line stripped of its white space chars so when the
|
|
// marker is removed in expansion, no white space is left from the line
|
|
// that has now been removed
|
|
(*line) = clean_line;
|
|
(*len) = new_len;
|
|
return true;
|
|
}
|
|
|
|
int Template::InsertLine(const char *line, int len, char* buffer) {
|
|
bool add_newline = (len > 0 && line[len-1] == '\n');
|
|
if (add_newline)
|
|
len--; // so we ignore the newline from now on
|
|
|
|
if (strip_ >= STRIP_WHITESPACE) {
|
|
StripWhiteSpace(&line, &len);
|
|
add_newline = false;
|
|
}
|
|
|
|
// IsBlankOrOnlyHasOneRemovableMarker may modify the two input
|
|
// parameters if the line contains only spaces or only one input
|
|
// marker. This modification must be done before the line is
|
|
// written to the input buffer. Hence the need for the boolean flag
|
|
// add_newline to be referenced after the Write statement.
|
|
if (strip_ >= STRIP_BLANK_LINES
|
|
&& IsBlankOrOnlyHasOneRemovableMarker(&line, &len)) {
|
|
add_newline = false;
|
|
}
|
|
|
|
memcpy(buffer, line, len);
|
|
|
|
if (add_newline) {
|
|
buffer[len++] = '\n';
|
|
}
|
|
return len;
|
|
}
|
|
|
|
int Template::InsertFile(const char *file, int len, char* buffer) {
|
|
const char* prev_pos = file;
|
|
const char* next_pos;
|
|
char* write_pos = buffer;
|
|
|
|
while ( (next_pos=(char*)memchr(prev_pos, '\n', file+len - prev_pos)) ) {
|
|
next_pos++; // include the newline
|
|
write_pos += InsertLine(prev_pos, next_pos - prev_pos, write_pos);
|
|
assert(write_pos - buffer <= len); // an invariant we guarantee
|
|
prev_pos = next_pos;
|
|
}
|
|
if (prev_pos < file + len) { // last line doesn't end in a newline
|
|
write_pos += InsertLine(prev_pos, file+len - prev_pos, write_pos);
|
|
assert(write_pos - buffer <= len);
|
|
}
|
|
return write_pos - buffer;
|
|
}
|
|
|
|
// ----------------------------------------------------------------------
|
|
// Template::Expand()
|
|
// This is the main function clients call: it expands a template
|
|
// by expanding its parse tree (which starts with a top-level
|
|
// section node). For each variable/section/include-template it
|
|
// sees, it replaces the name stored in the parse-tree with the
|
|
// appropriate value from the passed-in dictionary.
|
|
// ----------------------------------------------------------------------
|
|
|
|
void Template::Expand(ExpandEmitter *expand_emitter,
|
|
const TemplateDictionary *dict,
|
|
const TemplateDictionary *force_annotate_output) const {
|
|
// We hold mutex_ the entire time we expand, because
|
|
// ReloadIfChanged(), which also holds mutex_, is allowed to delete
|
|
// tree_, and we want to make sure it doesn't do that (in another
|
|
// thread) while we're expanding. We also protect state_, etc.
|
|
// Note we only need a read-lock here, so many expands can go on at once.
|
|
mutex_->LockRO();
|
|
|
|
if (state() != TS_READY) {
|
|
// We'd like to reload if state_ == TS_RELOAD, but we're a const method
|
|
mutex_->Unlock();
|
|
return;
|
|
}
|
|
|
|
const bool should_annotate = (dict->ShouldAnnotateOutput() ||
|
|
(force_annotate_output &&
|
|
force_annotate_output->ShouldAnnotateOutput()));
|
|
if (should_annotate) {
|
|
// Remove the machine dependent prefix from the template file name.
|
|
const char* file = template_file();
|
|
const char* short_file;
|
|
if (dict->ShouldAnnotateOutput()) {
|
|
short_file = strstr(file, dict->GetTemplatePathStart());
|
|
} else {
|
|
short_file = strstr(file, force_annotate_output->GetTemplatePathStart());
|
|
}
|
|
if (short_file != NULL) {
|
|
file = short_file;
|
|
}
|
|
expand_emitter->Emit(TemplateNode::OpenAnnotation("FILE", file));
|
|
}
|
|
|
|
// We force our sub-tree to annotate output if we annotate output
|
|
tree_->Expand(expand_emitter, dict, force_annotate_output);
|
|
|
|
if (should_annotate) {
|
|
expand_emitter->Emit(TemplateNode::CloseAnnotation("FILE"));
|
|
}
|
|
|
|
mutex_->Unlock();
|
|
}
|
|
|
|
void Template::Expand(string *output_buffer,
|
|
const TemplateDictionary *dict) const {
|
|
StringEmitter e(output_buffer);
|
|
Expand(&e, dict, NULL);
|
|
}
|
|
|
|
_END_GOOGLE_NAMESPACE_
|