Project Icon

jmustache

Java实现的Mustache模板语言库

jmustache是Java实现的Mustache模板语言库,具有零依赖、跨平台兼容和Proguard友好等特点。该库提供简洁API,支持部分渲染、Lambda表达式和自定义格式化。jmustache性能良好,可通过Maven Central集成。作为Java模板引擎,jmustache功能强大且易于使用。

这是一个Java实现的Mustache模板语言

构建状态

动机

  • 零依赖。你可以在项目中只包含这一个小型库就可以开始使用模板。

  • 可用于多种目标平台。该实现对JVM的要求非常有限,因此可以在Android或其他受限JVM上使用。甚至可以避免使用反射,而将所有数据作为一系列嵌套映射提供。

  • ProguardJarJar友好。虽然库会反射性地访问你的数据(如果你需要),但库内部不会使用其他反射或按名称实例化类。因此,你可以使用Proguard或JarJar嵌入它而不会遇到任何烦人的意外。

  • 最小API占用。你真正只需要了解两个方法:compileexecute。在性能不重要的情况下,你甚至可以将它们链接在一起。

在上述动机的基础上,该实现还努力提供额外的好处:

  • 它可通过Maven Central获得,详情见下文。
  • 它具有相当不错的性能。模板的解析与执行是分开的。模板会根据(上下文类,名称)对来专门化其变量,这样如果变量首次解析为(例如)上下文对象的字段,那么在后续的模板调用中会直接尝试这种方式,只有在访问变量作为字段失败时才会尝试较慢的完整解析。

获取

JMustache可通过Maven Central获得,因此可以通过添加依赖com.samskivert:jmustache:1.15轻松添加到你的Maven、Ivy等项目中。或者下载预构建的jar文件

文档

除了下面的使用部分,以下文档可能有用:

使用

使用JMustache非常简单。将你的模板作为StringReader提供,就可以得到一个可以在任何上下文中执行的Template:

String text = "一、二、{{three}}。三,先生!";
Template tmpl = Mustache.compiler().compile(text);
Map<String, String> data = new HashMap<String, String>();
data.put("three", "五");
System.out.println(tmpl.execute(data));
// 结果: "一、二、五。三,先生!"

如果你在做更严肃的事情,可以使用ReaderWriter:

void executeTemplate (Reader template, Writer out, Map<String, String> data) {
   Mustache.compiler().compile(template).execute(data, out);
}

执行上下文可以是任何Java对象。变量将通过以下机制解析:

  • 如果上下文是MustacheCustomContext,将使用MustacheCustomContext.get
  • 如果上下文是Map,将使用Map.get
  • 如果存在与变量同名的非void方法,将调用该方法。
  • 如果存在名为(对于变量foo)getFoo的非void方法,将调用该方法。
  • 如果存在与变量同名的字段,将使用其内容。

示例:

class Person {
    public final String name;
    public Person (String name, int age) {
        this.name = name;
        _age = age;
    }
    public int getAge () {
        return _age;
    }
    protected int _age;
}

String tmpl = "{{#persons}}{{name}}: {{age}}\n{{/persons}}";
Mustache.compiler().compile(tmpl).execute(new Object() {
    Object persons = Arrays.asList(new Person("Elvis", 75), new Person("Madonna", 52));
});

// 结果:
// Elvis: 75
// Madonna: 52

如你所见,字段(和方法)无需是public。在创建的匿名类中作为上下文的persons字段是可访问的。注意,在沙箱安全环境中,使用非公开字段将不起作用。

区段的行为如你所期望的:

  • Boolean值启用或禁用该区段。
  • 数组、IteratorIterable值会重复执行该区段,每次迭代使用每个元素作为上下文。空集合会导致该区段在模板中被包含零次。
  • 不可解析或null值被视为false。可以通过使用strictSections()来更改此行为。详见_默认值_。
  • 任何其他对象都会导致该区段执行一次,以该对象作为上下文。

请参阅MustacheTest.java中的代码以获取具体示例。另请参阅Mustache文档以了解模板语法的详细信息。

局部模板

如果你想使用局部模板(例如{{>subtmpl}}),在创建编译器时必须提供一个Mustache.TemplateLoader。例如:

final File templateDir = ...;
Mustache.Compiler c = Mustache.compiler().withLoader(new Mustache.TemplateLoader() {
    public Reader getTemplate (String name) {
        return new FileReader(new File(templateDir, name));
    }
});
String tmpl = "...{{>subtmpl}}...";
c.compile(tmpl).execute();

上面的代码片段在编译模板时将加载new File(templateDir, "subtmpl")

Lambda表达式

JMustache通过向你传递一个Template.Fragment实例来实现lambda表达式,你可以用它来执行传递给lambda的模板片段。你可以装饰片段执行的结果,如标准Mustache文档中关于lambda的示例所示:

String tmpl = "{{#bold}}{{name}}真棒。{{/bold}}";
Mustache.compiler().compile(tmpl).execute(new Object() {
   String name = "威利";
   Mustache.Lambda bold = new Mustache.Lambda() {
        public void execute (Template.Fragment frag, Writer out) throws IOException {
            out.write("<b>");
            frag.execute(out);
            out.write("</b>");
        }
    };
});
// 结果:
<b>威利真棒。</b>

你还可以获取片段执行的结果来进行国际化或缓存等操作:

Object ctx = new Object() {
    Mustache.Lambda i18n = new Mustache.Lambda() {
         public void execute (Template.Fragment frag, Writer out) throws IOException {
             String key = frag.execute();
             String text = // 在i18n系统中查找key
             out.write(text);
         }
    };
};
// 模板可能看起来像这样:
<h2>{{#i18n}}title{{/i18n}}</h2>
{{#i18n}}welcome_msg{{/i18n}}

还有对反编译(取消执行)模板并获取区段中包含的原始Mustache模板文本的有限支持。有关限制的详细信息,请参阅Template.Fragment的文档。

默认值

默认情况下,每当无法解析变量或变量解析为null时(区段除外,见下文),都会抛出异常。你可以通过两种方式更改此行为。如果你想在所有此类情况下提供一个值,请使用defaultValue():

String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().defaultValue("什么").compile(tmpl).execute(new Object() {
    String exists = "说";
    String nullValued = null;
    // String doesNotExist
});
// 结果:
说 什么 什么?

如果你只想为解析为null的变量提供默认值,并希望在无法解析变量的情况下保留异常,请使用nullValue():

String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().nullValue("什么").compile(tmpl).execute(new Object() {
    String exists = "说";
    String nullValued = null;
    // String doesNotExist
});
// 执行模板时会抛出MustacheException,因为无法解析doesNotExist

当使用Map作为上下文时,nullValue()仅在map包含映射到null的情况下使用。如果map缺少给定变量的映射,则认为它不可解析并抛出异常。

Map<String,String> map = new HashMap<String,String>();
map.put("exists", "说");
map.put("nullValued", null);
// 没有"doesNotExist"的映射
String tmpl = "{{exists}} {{nullValued}} {{doesNotExist}}?";
Mustache.compiler().nullValue("什么").compile(tmpl).execute(map);
// 执行模板时会抛出MustacheException,因为无法解析doesNotExist

不要在编译器配置中同时使用defaultValuenullValue。每个都会覆盖另一个,所以无论你最后调用哪个,都会得到那个行为。即使你意外地做对了,代码也会很混乱,所以不要同时调用两者,使用其中一个。

区段

区段不受nullValue()defaultValue()设置的影响。它们的行为由单独的配置strictSections()控制。

默认情况下,不可解析或解析为null的区段将被省略(相反,不可解析或解析为null的反向区段将被包含)。如果你使用strictSections(true),引用不可解析值的区段将始终抛出异常。无论strictSections()设置如何,引用可解析但为null值的区段永远不会抛出异常。

扩展

JMustache用一些额外的功能扩展了基本的Mustache模板语言。这些附加功能列举如下:

默认不转义HTML

你可以在获取编译器时更改默认的HTML转义行为:

Mustache.compiler().escapeHTML(false).compile("{{foo}}").execute(new Object() {
    String foo = "<bar>";
});
// 结果: <bar>
// 而不是: &lt;bar&gt;

用户定义的对象格式化

默认情况下,JMustache使用String.valueOf在渲染模板时将对象转换为字符串。你可以通过实现Mustache.Formatter接口来自定义此格式化:

Mustache.compiler().withFormatter(new Mustache.Formatter() {
    public String format (Object value) {
      if (value instanceof Date) return _fmt.format((Date)value);
      else return String.valueOf(value);
    }
    protected DateFormat _fmt = new SimpleDateFormat("yyyy/MM/dd");
}).compile("{{msg}}: {{today}}").execute(new Object() {
    String msg = "日期";
    Date today = new Date();
})
// 结果: 日期: 2013/01/08

用户定义的转义规则

你可以在获取编译器时更改转义行为,以支持HTML和纯文本以外的文件格式。

如果你只需要替换文本中的固定字符串,可以使用Escapers.simple:

String[][] escapes = {{ "[", "[[" }, { "]", "]]" }};
Mustache.compiler().withEscaper(Escapers.simple(escapes)).
  compile("{{foo}}").execute(new Object() {
      String foo = "[bar]";
  });
// 结果: [[bar]]

或者你可以直接实现Mustache.Escaper接口以更好地控制转义过程。

特殊变量

this

你可以使用特殊变量this来引用上下文对象本身,而不是它的某个成员。这在迭代列表时特别有用。

Mustache.compiler().compile("{{this}}").execute("你好"); // 返回: 你好
Mustache.compiler().compile("{{#names}}{{this}}{{/names}}").execute(new Object() {
    List<String> names () { return Arrays.asList("汤姆", "迪克", "哈利"); }
});
// 结果: 汤姆迪克哈利

请注意,你也可以使用特殊变量.来表示相同的意思。

Mustache.compiler().compile("{{.}}").execute("你好"); // 返回: 你好
Mustache.compiler().compile("{{#names}}{{.}}{{/names}}").execute(new Object() {
    List<String> names () { return Arrays.asList("汤姆", "迪克", "哈利"); }
});
// 结果: 汤姆迪克哈利

.显然受其他Mustache实现支持,尽管它没有出现在官方文档中。

-first和-last

你可以使用特殊变量-first-last对列表元素进行特殊处理。当在处理列表元素中的第一个元素的区段内时,-first解析为true。在所有其他时候,它解析为false。当在处理列表元素中的最后一个元素的区段内时,`-last

Mustache.compiler().compile("你好 {{field.who}}!").execute(new Object() {
    public Object field = new Object() {
        public String who () { return "世界"; }
    }
});
// 结果: 你好 世界!

通过利用反射和Bean属性风格的查找,你可以做一些奇怪的事情:

Mustache.compiler().compile("你好 {{class.name}}!").execute(new Object());
// 结果: 你好 java.lang.Object!

注意,复合变量本质上是使用单一部分的简写形式。上面的例子也可以表示为:

你好 {{#field}}{{who}}{{/field}}!
你好 {{#class}}{{name}}{{/class}}!

还要注意,嵌套的单一部分和复合变量之间存在一个语义差异:在解析复合变量的第一个组件的对象后,在解析子组件时不会搜索父上下文。

换行符修剪

如果开始或结束部分标签是一行中唯一的内容,则会修剪标签周围的任何空白和后面的行终止符。这允许文明的模板,比如:

最喜欢的食物:
<ul>
  {{#people}}
  <li>{{first_name}} {{last_name}} 喜欢 {{favorite_food}}。</li>
  {{/people}}
</ul>

它会产生如下输出:

最喜欢的食物:
<ul>
  <li>埃尔维斯 普雷斯利 喜欢 花生酱。</li>
  <li>圣雄 甘地 喜欢 阿卢杜姆。</li>
</ul>

而不是:

最喜欢的食物:
<ul>

  <li>埃尔维斯 普雷斯利 喜欢 花生酱。</li>

  <li>圣雄 甘地 喜欢 阿卢杜姆。</li>

</ul>

如果没有换行符修剪,就会产生这样的结果。

嵌套上下文

如果在嵌套上下文中找不到变量,它会在外层上下文中解析。这允许以下用法:

String template = "{{outer}}:\n{{#inner}}{{outer}}.{{this}}\n{{/inner}}";
Mustache.compiler().compile(template).execute(new Object() {
    String outer = "foo";
    List<String> inner = Arrays.asList("bar", "baz", "bif");
});
// 结果:
// foo:
// foo.bar
// foo.baz
// foo.bif

注意,如果变量在内部上下文中定义,它会覆盖外部上下文中的同名变量。目前没有办法访问外部上下文中的变量。

可反转的Lambda

对于某些应用程序,Lambda在反向部分执行而不是完全省略该部分可能很有用。这允许在将模板静态转换为其他语言或上下文时进行适当的条件替换:

String template = "{{#condition}}结果为真{{/condition}}\n" +
  "{{^condition}}结果为假{{/condition}}";
Mustache.compiler().compile(template).execute(new Object() {
    Mustache.InvertibleLambda condition = new Mustache.InvertibleLambda() {
        public void execute (Template.Fragment frag, Writer out) throws IOException {
            // 当Lambda在正常部分中被引用时执行此方法
            out.write("if (condition) {console.log(\"");
            out.write(toJavaScriptLiteral(frag.execute()));
            out.write("\")}");
        }
        public void executeInverse (Template.Fragment frag, Writer out) throws IOException {
            // 当Lambda在反向部分中被引用时执行此方法
            out.write("if (!condition) {console.log(\"");
            out.write(toJavaScriptLiteral(frag.execute()));
            out.write("\")}");
        }
        private String toJavaScriptLiteral (String execute) {
            // 注意:这不是JavaScript字符串字面量转义的完整实现
            return execute.replaceAll("\\\\", "\\\\\\\\").replaceAll("\"", "\\\\\"");
        }
    };
});
// 结果:
// if (condition) {console.log("结果为真")}
// if (!condition) {console.log("结果为假")}

当然,你不仅限于条件替换 - 只要需要具有两种操作模式的单个函数,就可以使用InvertibleLambda。

标准模式

在创建编译器时,可以禁用这些扩展中更具侵入性的部分,特别是搜索父上下文和使用复合变量,如下所示:

Map<String,String> ctx = new HashMap<String,String>();
ctx.put("foo.bar", "baz");
Mustache.compiler().standardsMode(true).compile("{{foo.bar}}").execute(ctx);
// 结果: baz

线程安全

JMustache在内部是线程安全的,但有以下注意事项:

  • 编译:编译模板会调用各种辅助类: Mustache.Formatter, Mustache.Escaper, Mustache.TemplateLoader, Mustache.Collector。这些类的默认实现是线程安全的,但如果你提供自定义实例,则必须确保你的自定义实例是线程安全的。

  • 执行:执行模板可能会调用一些辅助类:Mustache.Lambda, Mustache.VariableFetcher。这些类的默认实现是线程安全的,但如果你提供自定义实例,则必须确保你的自定义实例是线程安全的。

  • 上下文数据:如果在执行模板时修改传递给模板执行的上下文数据,则会引入竞态条件。理论上可以使用线程安全的映射(ConcurrentHashMapCollections.synchronizedMap)作为上下文数据,这将允许你在基于该数据渲染模板的同时修改数据,但这样做是在玩火。我不建议这样做。如果你的数据是作为POJO提供的,其中字段或方法通过反射被调用以填充模板,那么volatile字段和同步方法同样可以用来支持同时读取和修改,但是你很容易犯错误引入竞态条件或在执行模板时导致奇怪的行为。当通过同时运行的线程渲染相同的模板时,最安全的方法是将不可变/不变的数据作为每次执行的上下文传递。

  • VariableFetcher缓存:模板执行使用一个内部缓存来存储已解析的VariableFetcher实例(因为解析变量获取器是昂贵的)。这个缓存通过使用ConcurrentHashMap来实现线程安全。如果两个线程同时解析同一个变量,可能会做一些额外的工作,但它们不会相互冲突,它们只会都解析该变量,而不是一个解析变量而另一个使用缓存的解析结果。

所以总结是:只要你提供的所有辅助类都是线程安全的(或者你使用默认值),在线程之间共享一个Mustache.Compiler实例来编译模板是安全的。如果你在执行时向模板传递不可变数据,那么让多个线程同时执行单个Template实例是安全的。

限制

为了简单起见,省略或简化了Mustache的一些功能:

  • {{= =}}只支持一个或两个字符的分隔符。这只是因为我很懒,它简化了解析器。
项目侧边栏1项目侧边栏2
推荐项目
Project Cover

豆包MarsCode

豆包 MarsCode 是一款革命性的编程助手,通过AI技术提供代码补全、单测生成、代码解释和智能问答等功能,支持100+编程语言,与主流编辑器无缝集成,显著提升开发效率和代码质量。

Project Cover

AI写歌

Suno AI是一个革命性的AI音乐创作平台,能在短短30秒内帮助用户创作出一首完整的歌曲。无论是寻找创作灵感还是需要快速制作音乐,Suno AI都是音乐爱好者和专业人士的理想选择。

Project Cover

有言AI

有言平台提供一站式AIGC视频创作解决方案,通过智能技术简化视频制作流程。无论是企业宣传还是个人分享,有言都能帮助用户快速、轻松地制作出专业级别的视频内容。

Project Cover

Kimi

Kimi AI助手提供多语言对话支持,能够阅读和理解用户上传的文件内容,解析网页信息,并结合搜索结果为用户提供详尽的答案。无论是日常咨询还是专业问题,Kimi都能以友好、专业的方式提供帮助。

Project Cover

阿里绘蛙

绘蛙是阿里巴巴集团推出的革命性AI电商营销平台。利用尖端人工智能技术,为商家提供一键生成商品图和营销文案的服务,显著提升内容创作效率和营销效果。适用于淘宝、天猫等电商平台,让商品第一时间被种草。

Project Cover

吐司

探索Tensor.Art平台的独特AI模型,免费访问各种图像生成与AI训练工具,从Stable Diffusion等基础模型开始,轻松实现创新图像生成。体验前沿的AI技术,推动个人和企业的创新发展。

Project Cover

SubCat字幕猫

SubCat字幕猫APP是一款创新的视频播放器,它将改变您观看视频的方式!SubCat结合了先进的人工智能技术,为您提供即时视频字幕翻译,无论是本地视频还是网络流媒体,让您轻松享受各种语言的内容。

Project Cover

美间AI

美间AI创意设计平台,利用前沿AI技术,为设计师和营销人员提供一站式设计解决方案。从智能海报到3D效果图,再到文案生成,美间让创意设计更简单、更高效。

Project Cover

AIWritePaper论文写作

AIWritePaper论文写作是一站式AI论文写作辅助工具,简化了选题、文献检索至论文撰写的整个过程。通过简单设定,平台可快速生成高质量论文大纲和全文,配合图表、参考文献等一应俱全,同时提供开题报告和答辩PPT等增值服务,保障数据安全,有效提升写作效率和论文质量。

投诉举报邮箱: service@vectorlightyear.com
@2024 懂AI·鲁ICP备2024100362号-6·鲁公网安备37021002001498号