步入 KumaScript

这篇翻译不完整。请帮忙从英语翻译这篇文章

概述

在提供 MDN 的 Kuma 平台上,wiki 上进行系统自动化方面的模板系统被称为 KumaScript。KumaScript 是服务端运行的 JavaScript,采用 Node.js 实现。本文将介绍如何使用 KumaScript 的基础信息。

有关 KumaScript 的详细概述和问答信息,请查阅 MDN 开发团队的 KumaScript Fireside Chat(会议开始于视频的第10分钟)。KumaScript 已取代 MDN 以前使用的 DekiScript,它是用于 MindTouch 的模板语言。

KumaScript 是什么?

  • 一种重用和本地化在多个文档中重复出现的内容的方法(例如兼容性标签、章节导航、警告条幅)。
  • 一种从其他文档提取内容以构建文档的方法。
  • 一种从其他网站或服务(例如 Bugzilla)摘取内容的方法。

KumaScript 不是什么

  • KumaScript 不支持接受表单提交的交互式脚本。
  • KumaScript 不访问数据库、文件或以其他方式永久存储信息。
  • KumaScript 不支持基于当前登录的用户进行网站个性化。
  • KumaScript 不能访问用户信息,只能访问目前查看的 Wiki 页面的内容和元数据。

基础

KumaScript 允许可信任的 MDN 用户编写嵌入式 JavaScript 模板。这些模板可以由任何 MDN 作者在文档内容中使用宏来调用。

以 KumaScript 编写的脚本为一个模板,每个模板是KumaScript 的 macros 目录中的一个文件。模板像是这样:

<% for (var i = 0; i < $0; i++) { %> 
Hello #<%= i %> 
<% } %>

调用一个模板是采用宏(macro)来完成,它可以在任何 wiki 内容的任何地方使用。宏如下所示:

{{ hello("3") }}

上述宏的输出是这样:

Hello #0
Hello #1
Hello #2

宏语法

在文档内容中使用宏调用 KumaScript 模板时像是这样:

{{ 模板名称("参数0", "参数1", ..., "参数N") }}

宏语法按下列规则组成:

  • 宏以 {{ 开始,以 }} 结束。
  • 宏的第一部分是模板名称。此名称的小写值应与 KumaScript 的宏目录中某个文件名的小写值匹配。
  • 模板可以接受参数,参数列表以括号开始和结束。
  • All non-numeric parameters must be in quotes. Numbers can be left unquoted.

使用 JSON 作为宏参数

As a semi-experimental feature (not guaranteed to work), you can supply a JSON object for the first and only parameter, like so:

{{ templateName({ "Alpha":"one", "Beta":["a","b","c"], "Foo":"http:\/\/mozilla.org\/" }) }}

The data from this macro is available in template code as an object in the $0 argument (e.g., $0.Alpha, $0.Beta, $0.Foo). This also allows you to express complex data structures in macro parameters that are hard or impossible to do with a simple list of parameters.

Note that this parameter style is very picky — it must adhere to JSON syntax exactly, which has some requirements about escaping characters that are easy to miss (e.g., all forward slashes are escaped). When in doubt, try running your JSON through a validator.

如何在文本中写出 "{{"

Since the character sequence "{{" is used to indicate the start of a macro, this can be troublesome if you actually just want to use "{{" and "}}" in a page. It will probably produce DocumentParsingError messages.

In this case, you can escape the first brace with a backslash, like so: \{

模板语法

Each KumaScript template is a file under the macros directory of KumaScript. You create and edit these files as you would the files of any open-source project on GitHub (see the KumaScript README for more information).

KumaScript templates are processed by an embedded JavaScript template engine with a few simple rules:

  • Within a template, the parameters passed in from the macro are available as the variables $0, $1, $2, and so on. The entire list of parameters is also available in a template as the variable arguments.
  • Most text is treated as output and included in the output stream.
  • JavaScript variables and expressions can be inserted into the output stream with these blocks:
    • <%= expr %> — the value of a JavaScript expression is escaped for HTML before being included in output (e.g., characters like < and > are turned into &lt; and &gt;).
    • <%- expr %> — the value of a JavaScript expression is included in output without any escaping. (Use this if you want to dynamically build markup or use the results of another template that may include markup.)
    • It is an error to include semicolons inside these blocks.
  • Anything inside a <% %> block is interpreted as JavaScript. This can include loops, conditionals, etc.
  • Nothing inside a <% %> block can ever contribute to the output stream. But, you can transition from JS mode to output mode using <% %>—for example:
    <% for (var i = 0; i < $0; i++) { %>
    Hello #<%= i %>
    <% } %>
    

    Note how the JavaScript code is contained in <% ... %>, and output happens in the space between %> ... <%. The for loop in JS can begin with one <% %> block, transition to output mode, and finish up in a second <% %> JS block.

  • For more details on EJS syntax, check out the upstream module documentation.

提示

日期和时间

It's important to note that the standard JavaScript Date constructor is overridden by the KumaScript Date interface. You can create a JavaScript Date by calling the KumaScript date.now() or date.parse() function.

高级特性

Beyond the basics, the KumaScript system offers some advanced features.

环境变量

When the wiki makes a call to the KumaScript service, it passes along some context on the current document that KumaScript makes available to templates as variables:

env.path
当前 wiki 文档的路径
env.url
当前 wiki 文档的完整 URL
env.id
当前 wiki 文档的简短、唯一性 ID
env.files
An array of the files attached to the current wiki document; each object in the array is as described under File objects below
env.review_tags
An array of the review tags on the article ("technical", "editorial", etc.)
env.locale
当前 wiki 文档的语言区域
env.title
当前 wiki 文档的标题
env.slug
当前 wiki 文档的 URL slug
env.tags
An array list of tag names for the current wiki document
env.modified
当前 wiki 文档的上次修改时间戳
env.cache_control
Cache-Control header sent in the request for the current wiki document, useful in deciding whether to invalidate caches

文件对象

每个文件对象有下列字段:

title
附件的标题。
description
A textual description of the current revision of the file
filename
文件名
size
文件的字节大小
author
The username of the person who uploaded the file
mime
文件的 MIME 类型
url
The URL at which the file can be found

使用标签列表

The env.tags and env.review_tags variables return arrays of tags. You can work with these in many ways, of course, but here are a couple of suggestions.

查看指定标签是否已设置

You can look to see if a specific tag exists on a page like this:

if (env.tags.indexOf("tag") != −1) {
  // The page has the tag "tag"
}
Iterating over all the tags on a page

You can also iterate over all the tags on a page, like this:

env.tag.forEach(function(tag) {
  // do whatever you need to do, such as:
  if (tag.indexOf("a") == 0) {
    // this tag starts with "a" - woohoo!
  }
});

API 和模块

KumaScript offers some built-in utility APIs, as well as the ability to define new APIs in modules editable as wiki documents.

内置方法

This manually-maintained documentation is likely to fall out of date with the code. With that in mind, you can always check out the latest state of built-in APIs in the KumaScript source. But here is a selection of useful methods exposed to templates:

md5(string)
Returns an MD5 hex digest of the given string.
template("name", ["arg0", "arg1", ..., "argN"])
Executes and returns the result of the named template with the given list of parameters.
Example: <%- template("warning", ["foo", "bar", "baz"]) %>.
Example using the domxref macro: <%- template("domxref", ["Event.bubbles", "bubbles"]) %>.
This is a JavaScript function. So, if one of the parameters is an arg variable like $2, do not put it in quotes. Like this: <%- template("warning", [$1, $2, "baz"]) %>. If you need to call another template from within a block of code, do not use <% ... %>. Example: myvar = "<li>" + template("LXRSearch", ["ident", "i", $1]) + "</li>";
require(name)
Loads another template as a module; any output is ignored. Anything assigned to module.exports in the template is returned.
Used in templates like so: <% var my_module = require('MyModule'); %>.
cacheFn(key, timeout, function_to_cache)
Using the given key and cache entry lifetime, cache the results of the given function. Honors the value of env.cache_control to invalidate cache on no-cache, which can be sent by a logged-in user hitting shift-refresh.
request
Access to mikeal/request, a library for making HTTP requests. Using this module in KumaScript templates is not yet very friendly, so you may want to wrap usage in module APIs that simplify things.
log.debug(string)
Outputs a debug message into the script log on the page (i.e. the big red box that usually displays errors).

Built-in API modules

There's only one API built in at the moment, in the kuma namespace. You can see the most up to date list of methods under kuma from the KumaScript source code, but here are a few:

kuma.inspect(object)
Renders any JS object as a string, handy for use with log.debug(). See also: node.js util.inspect().
kuma.htmlEscape(string)
Escapes the characters &, <, >, " to &amp, &lt;, &gt;, &quot;, respectively.
kuma.url
See also: node.js url module.
kuma.fetchFeed(url)
Fetch an RSS feed and parse it into a JS object. See also: InsertFeedLinkList

创建模块

Using the built-in require() method, you can load a template as a module to share common variables and methods between templates. A module can be defined in a template like this:

<%
module.exports = {
    add: function (a, b) {
        return a + b;
    }
}
%>

Assuming this template is saved under https://github.com/mozilla/kumascript/tree/master/macros as MathLib.ejs, you can use it in another template like so:

<%
var math_lib = require("MathLib");
%>
The result of 2 + 2 = <%= math_lib.add(2, 2) %>

And, the output of this template will be:

The result of 2 + 2 = 4

自动加载模块

There are a set of modules editable as wiki templates that are automatically loaded and made available to every template. This set is defined in the configuration file for the KumaScript service - any changes to this requires an IT bug to edit configuration and a restart of the service.

For the most part, these attempt to provide stand-ins for legacy DekiScript features to ease template migration. But, going forward, these can be used to share common variables and methods between templates:

Note: You might notice that the DekiScript modules use a built-in method named buildAPI(), like so:

<% module.exports = buildAPI({
    StartsWith: function (str, sub_str) {
        return (''+str).indexOf(sub_str) === 0;
    }
}); %>

The reason for this is because DekiScript is case-insensitive when it comes to references to API methods, whereas JavaScript is strict about uppercase and lowercase in references. So, buildAPI() is a hack to try to cover common case variations in DekiScript calls found in legacy templates.

With that in mind, please do not use buildAPI() in new modules.

提示和警告

调试

A useful tip when debugging. You can use the log.debug() method to output text to the scripting messages area at the top of the page that's running your template. Note that you need to be really sure to remove these when you're done debugging, as they're visible to all users! To use it, just do something like this:

<%- log.debug("Some text goes here"); %>

You can, of course, create more complex output using script code if it's helpful.

缓存

KumaScript templates are heavily cached to improve performance. For the most part, this works great to serve up content that doesn't change very often. But, as a logged-in user, you have two options to force a page to be regenerated, in case you notice issues with scripting:

  • Hit Refresh in your browser. This causes KumaScript to invalidate its cache for the content on the current page by issuing a request with a Cache-Control: max-age=0 header.
  • Hit Shift-Refresh in your browser. This causes KumaScript to invalidate cache for the current page, as well as for any templates or content used by the current page by issuing a request with a Cache-Control: no-cache header.

使用搜索关键词打开模板页面

When using templates, it's common to open the template's code in a browser window to review the comments at the top, which are used to document the template, its parameters, and how to use it properly. To quickly access templates, you can create a Firefox search keyword, which gives you a shortcut you can type in the URL box to get to a template more easily.

To create a search keyword, open the bookmarks window by choosing "Show all bookmarks" in the Bookmarks menu, or by pressing Control-Shift-B (Command-Shift-B on Mac). Then from the utility ("Gear") menu in the Library window that appears, choose "New Bookmark...".

This causes the bookmark editing dialog to appear. Fill that out as follows:

Name
A suitable name for your search keyword; "Open MDN Template" is a good one.
Location
https://github.com/mozilla/kumascript/blob/master/macros/%s
Tags 可选
A list of tags used to organize your bookmarks; these are entirely optional and what (if any) tags you use is up to you.
Keyword
The shortcut text you wish to use to access the template. Ideally, this should be something short and quick to type, such as simply "t" or "mdnt".
Description 可选
A suitable description explaining what the search keyword does.

The resulting dialog looks something like this:

Then click the "Add" button to save your new search keyword. From then on, typing your keyword, then a space, then the name of a macro will open that macro in your current tab. So if you used "t" as the keyword, typing t ListSubpages will show you the page at ListSubpages.

技巧

This section will list examples of common patterns for templates used on MDN, including samples of legacy DekiScript templates and their new KumaScript equivalents.

强制重新加载页面上使用的模板

It bears repeating: To force templates used on a page to be reloaded after editing, hit Shift-Reload. Just using Reload by itself will cause the page contents to be regenerated, but using cached templates and included content. A Shift-Reload is necessary to invalidate caches beyond just the content of the page itself.

应对“未知错误”

Sometimes, you'll see a scripting message like this when you load a page:

Kumascript service failed unexpectedly: <class 'httplib.BadStatusLine'>

This is probably a temporary failure of the KumaScript service. If you Refresh the page, the error may disappear. If that doesn't work, try a Shift-Refresh. If, after a few tries, the error persists - file an IT bug for Mozilla Developer Network to ask for an investigation.

故障的 wiki.languages() 宏

On some pages, you'll see a scripting error like this:

Syntax error at line 436, column 461: Expected valid JSON object as the parameter of the preceding macro but...

If you edit the page, you'll probably see a macro like this at the bottom of the page:

{{ wiki.languages({ "zh-tw": "zh_tw/Core_JavaScript_1.5_教學/JavaScript_概要", ... }) }}

To fix the problem, just delete the macro. Or, replace the curly braces on either side with HTML comments <!-- --> to preserve the information, like so:

<!-- wiki.languages({ "zh-tw": "zh_tw/Core_JavaScript_1.5_教學/JavaScript_概要", ... }) -->

Because Kuma supports localization differently, these macros aren't actually needed any more. But, they've been left intact in case we need to revisit the relationships between localized pages. Unfortunately, it seems like migration has failed to convert some of them properly.

查询当前页面的语言

In KumaScript, the locale of the current document is exposed as an environment variable:

var lang = env.locale;

The env.locale variable should be reliable and defined for every document.

读取页面附件的内容

You can read the contents of an attached file by using the mdn.getFileContent() function, like this:

<%
  var contents = mdn.getFileContent(fileUrl);
  ... do stuff with the contents ...
%>

or

<%-mdn.getFileContent(fileObject)%>

In other words, you may specify either the URL of the file to read or as a file object. The file objects for a page can be accessed through the array env.files. So, for example, to embed the contents of the first file attached to the article, you can do this:

<%-mdn.getFileContent(env.files[0])%>
Note: You probably don't want to try to embed the contents of a non-text file this way, as the raw contents would be injected as text. This is meant to let you access the contents of text attachments.

If the file isn't found, an empty string is returned. There is currently no way to tell the difference between an empty file and a nonexistent one. But if you're putting empty files on the wiki, you're doing it wrong.

本地化模板内容

Templates are not translated like wiki pages, rather any single template might be used for any number of locales.

So the main way to output content tailored to the current document locale is to pivot on the value of env.locale. There are many ways to do this, but a few patterns are common in the conversion of legacy DekiScript templates:

KumaScript 中的 If/else 块

The KumaScript equivalent of this can be achieved with simple if/else blocks, like so:

<% if ("fr" == env.locale) { %>
<%- template("CSSRef") %> « <a title="Référence_CSS/Extensions_Mozilla" href="/fr/docs/Référence_CSS/Extensions_Mozilla">Référence CSS:Extensions Mozilla</a>
<% } else if ("ja" == env.locale) { %>
<%- template("CSSRef") %> « <a title="CSS_Reference/Mozilla_Extensions" href="/ja/docs/CSS_Reference/Mozilla_Extensions">CSS リファレンス:Mozilla 拡張仕様</a>
<% } else if ("pl" == env.locale) { %>
<%- template("CSSRef") %> « <a title="Dokumentacja_CSS/Rozszerzenia_Mozilli" href="/pl/docs/Dokumentacja_CSS/Rozszerzenia_Mozilli">Dokumentacja CSS:Rozszerzenia Mozilli</a>
<% } else if ("de" == env.locale) { %>
<%- template("CSSRef") %> « <a title="CSS_Referenz/Mozilla_CSS_Erweiterungen" href="/de/docs/CSS_Referenz/Mozilla_CSS_Erweiterungen">CSS Referenz: Mozilla Erweiterungen</a>
<% } else { %>
<%- template("CSSRef") %> « <a title="CSS_Reference/Mozilla_Extensions" href="/en-US/docs/CSS_Reference/Mozilla_Extensions">CSS Reference:Mozilla Extensions</a>
<% } %>

Depending on what text editor is your favorite, you may be able to copy & paste from the browser-based editor and attack this pattern with a series of search/replace regexes to get you most of the way there.

My favorite editor is MacVim, and a series of regexes like this does the bulk of the work with just a little manual clean up following:

%s#<span#^M<span#g
%s#<span lang="\(.*\)" .*>#<% } else if ("\1" == env.locale) { %>#g
%s#<span class="script">template.Cssxref(#<%- template("Cssxref", [#
%s#)</span> </span>#]) %>

Your mileage may vary, and patterns change slightly from template to template. That's why the migration script was unable to just handle this automatically, after all.

字符串变量与 switch

Rather than switch between full chunks of markup, you can define a set of strings, switch them based on locale, and then use them to fill in placeholders in a single chunk of markup:

<%
var s_title = 'Firefox for Developers';
switch (env.locale) {
    case 'de':
        s_title = "Firefox für Entwickler";
        break;
    case 'fr':
        s_title = "Firefox pour les développeurs";
        break;
    case 'es':
        s_title = "Firefox para desarrolladores";
        break;
};
%>
<span class="title"><%= s_title %></span>

使用 mdn.localString()

mdn.localString() 最近被加入到 MDN:Common 模块,其使用方法如下:

<%
var s_title = mdn.localString({
  "en-US": "Firefox for Developers",
  "de": "Firefox für Entwickler",
  "es": "Firefox para desarrolladores"
});
%>
<span class="title"><%= s_title %></span>

This is more concise than the switch statement, and may be a better choice where a single string is concerned. However, if many strings need to be translated (e.g., as in CSSRef), a switch statement might help keep all the strings grouped by locale and more easily translated that way.

When the object does not have the appropriate locale, the value of "en-US" is used as the initial value.

另见

文档标签和贡献者

 此页面的贡献者: yfdyh000, ziyunfei
 最后编辑者: yfdyh000,