Mail footer discoverability helper
Add info regarding mail footers and filter authoring to PolyGerrit and
to GWT UI settings screens and include a mention in the change footer
template.
Feature: Issue 4982
Change-Id: I3443af49fcdc16ca941ee7cf2b5e33c1106f3b1d
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index 4dc4880..d83ca87 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -142,6 +142,108 @@
access is automatically checked by Gerrit and therefore does not
need to use the `visibleto:` operator in the filter.
+[[footers]]
+== Email Footers
+
+Notification emails related to changes include metadata about the change
+to support writing mail filters. This metadata is included in the form
+of footers in the message content. For HTML emails, these footers are
+hidden, but they can be examined by viewing the HTML source of messages.
+
+In this way users may apply filters and rules to their incoming Gerrit
+notifications using the values of these footers. For example a Gmail
+filter to find emails regarding reviews that you are a reviewer of might
+take the following form.
+
+----
+ "Gerrit-Reviewer: Your Name <your.email@example.com>"
+----
+
+[[Gerrit-MessageType]]Gerrit-MessageType::
+
+The message type footer states the type of the message and will take one
+of the following values.
+
+* abandon
+* comment
+* deleteReviewer
+* deleteVote
+* merged
+* newchange
+* newpatchset
+* restore
+* revert
+* setassignee
+
+[[Gerrit-Change-Id]]Gerrit-Change-Id::
+
+The change ID footer states the ID of the change, such as
+`I3443af49fcdc16ca941ee7cf2b5e33c1106f3b1d`.
+
+[[Gerrit-Change-Number]]Gerrit-Change-Number::
+
+The change number footer states the numeric ID of the change, for
+example `92191`.
+
+[[Gerrit-PatchSet]]Gerrit-PatchSet::
+
+The patch set footer states the number of the patch set that the email
+relates to. For example, a notification email for a vote being set on
+the seventh patch set will take a value of `7`.
+
+[[Gerrit-Owner]]Gerrit-Owner::
+
+The owner footer states the name and email address of the change's
+owner. For example, `Owner Name <owner@example.com>`.
+
+[[Gerrit-Reviewer]]Gerrit-Reviewer::
+
+The reviewer footers list the names and email addresses of the change's
+reviewrs. One footer is included for each reviewer. For example, if a
+change has two reviewers, the footers might include:
+
+----
+ Gerrit-Reviewer: Reviewer One <one@example.com>
+ Gerrit-Reviewer: Reviewer Two <two@example.com>
+----
+
+[[Gerrit-CC]]Gerrit-CC::
+
+The CC footers list the names and email addresses of those who have been
+CC'd on the change. One footer is included for each reviewer. For
+example, if a change CCs two users, the footers might include:
+
+----
+ Gerrit-CC: User One <one@example.com>
+ Gerrit-CC: User Two <two@example.com>
+----
+
+[[Gerrit-Project]]Gerrit-Project::
+
+The project footer states the project to which the change belongs.
+
+[[Gerrit-Branch]]Gerrit-Branch::
+
+The branch footer states the abbreviated name of the branch that the
+change targets.
+
+[[Gerrit-Comment-Date]]Gerrit-Comment-Date::
+
+In comment emails, the comment date footer states the date that the
+comment was posted.
+
+[[Gerrit-HasComments]]Gerrit-HasComments::
+
+In comment emails, the has-comments footer states whether inline
+comments had been posted in that notification using "Yes" or "No", for
+example `Gerrit-HasComments: Yes`.
+
+[[Gerrit-HasLabels]]Gerrit-HasLabels::
+
+In comment emails, the has-labels footer states whether label votes had
+been posted in that notification using "Yes" or "No", for
+example `Gerrit-HasLabels: No`.
+
GERRIT
------
Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 314871e..9799723 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -239,6 +239,10 @@
String errorDialogTitleRegisterNewEmail();
+ String emailFilterHelpTitle();
+
+ String emailFilterHelp();
+
String newAgreement();
String agreementName();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index d31abdb..a975b29 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -158,7 +158,74 @@
<p>A confirmation link will be sent by email to this address.</p>\
<p>You must click on the link to complete the registration and make the address available for selection.</p>
errorDialogTitleRegisterNewEmail = Email Registration Failed
-
+emailFilterHelpTitle = Mail Filters
+emailFilterHelp = \
+ <p>\
+ Gerrit emails include metadata about the change to support \
+ writing mail filters.\
+ </p>\
+ <p>\
+ Here are some example Gmail queries that can be used for filters or \
+ for searching through archived messages. View the \
+ <a href="https://gerrit-review.googlesource.com/Documentation/user-notify.html"\
+ target="_blank" rel="nofollow">Gerrit documentation</a> for \
+ the complete set of footers.\
+ </p>\
+ <table>\
+ <tbody>\
+ <tr><th>Name</th><th>Query</th></tr>\
+ <tr>\
+ <td>Changes requesting my review</td>\
+ <td>\
+ <code>\
+ "Gerrit-Reviewer: <em>Your Name</em>\
+ <<em>your.email@example.com</em>>"\
+ </code>\
+ </td>\
+ </tr>\
+ <tr>\
+ <td>Changes from a specific owner</td>\
+ <td>\
+ <code>\
+ "Gerrit-Owner: <em>Owner name</em>\
+ <<em>owner.email@example.com</em>>"\
+ </code>\
+ </td>\
+ </tr>\
+ <tr>\
+ <td>Changes targeting a specific branch</td>\
+ <td>\
+ <code>\
+ "Gerrit-Branch: <em>branch-name</em>"\
+ </code>\
+ </td>\
+ </tr>\
+ <tr>\
+ <td>Changes in a specific project</td>\
+ <td>\
+ <code>\
+ "Gerrit-Project: <em>project-name</em>"\
+ </code>\
+ </td>\
+ </tr>\
+ <tr>\
+ <td>Messages related to a specific Change ID</td>\
+ <td>\
+ <code>\
+ "Gerrit-Change-Id: <em>Change ID</em>"\
+ </code>\
+ </td>\
+ </tr>\
+ <tr>\
+ <td>Messages related to a specific change number</td>\
+ <td>\
+ <code>\
+ "Gerrit-Change-Number: <em>change number</em>"\
+ </code>\
+ </td>\
+ </tr>\
+ </tbody>\
+ </table>
newAgreement = New Contributor Agreement
agreementName = Name
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index cbd7635..a537063 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -21,6 +21,7 @@
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.errors.EmailException;
@@ -153,6 +154,11 @@
}
});
+ final ComplexDisclosurePanel mailFilterHelp =
+ new ComplexDisclosurePanel(Util.C.emailFilterHelpTitle(), false);
+ mailFilterHelp.setContent(new HTML(Util.C.emailFilterHelp()));
+ body.add(mailFilterHelp);
+
emailPick.addChangeHandler(
new ChangeHandler() {
@Override
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
index a034872..37ac126 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.soy
@@ -30,7 +30,8 @@
{/if}
{if $email.settingsUrl}
- To unsubscribe, visit {$email.settingsUrl}{\n}
+ To unsubscribe, or for help writing mail filters,{sp}
+ visit {$email.settingsUrl}{\n}
{/if}
{if $email.changeUrl or $email.settingsUrl}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
index 61feb57..00f21db 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooterHtml.soy
@@ -29,7 +29,8 @@
{/if}
{if $email.changeUrl and $email.settingsUrl}{sp}{/if}
{if $email.settingsUrl}
- To unsubscribe, visit <a href="{$email.settingsUrl}">settings</a>.
+ To unsubscribe, or for help writing mail filters,{sp}
+ visit <a href="{$email.settingsUrl}">settings</a>.
{/if}
</p>
{/if}
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html
new file mode 100644
index 0000000..4074c3b
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior.html
@@ -0,0 +1,63 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<link rel="import" href="../base-url-behavior/base-url-behavior.html">
+<script>
+(function(window) {
+ 'use strict';
+
+ const PROBE_PATH = '/Documentation/index.html';
+ const DOCS_BASE_PATH = '/Documentation';
+
+ let cachedPromise;
+
+ /** @polymerBehavior Gerrit.DocsUrlBehavior */
+ const DocsUrlBehavior = {
+
+ /**
+ * Get the docs base URL from either the server config or by probing.
+ * @param {Object} config The server config.
+ * @param {!Object} restApi A REST API instance
+ * @return {!Promise<String>} A promise that resolves with the docs base
+ * URL.
+ */
+ getDocsBaseUrl(config, restApi) {
+ if (!cachedPromise) {
+ cachedPromise = new Promise(resolve => {
+ if (config && config.gerrit && config.gerrit.doc_url) {
+ resolve(config.gerrit.doc_url);
+ } else {
+ restApi.probePath(this.getBaseUrl() + PROBE_PATH).then(ok => {
+ resolve(ok ? (this.getBaseUrl() + DOCS_BASE_PATH) : null);
+ });
+ }
+ });
+ }
+ return cachedPromise;
+ },
+
+ /** For testing only. */
+ _clearDocsBaseUrlCache() {
+ cachedPromise = undefined;
+ },
+ };
+
+ window.Gerrit = window.Gerrit || {};
+ window.Gerrit.DocsUrlBehavior = [
+ window.Gerrit.BaseUrlBehavior,
+ DocsUrlBehavior,
+ ];
+})(window);
+</script>
diff --git a/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
new file mode 100644
index 0000000..8154c78
--- /dev/null
+++ b/polygerrit-ui/app/behaviors/docs-url-behavior/docs-url-behavior_test.html
@@ -0,0 +1,98 @@
+<!--
+Copyright (C) 2017 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!-- Polymer included for the html import polyfill. -->
+<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
+<script src="../../bower_components/web-component-tester/browser.js"></script>
+<link rel="import" href="../../test/common-test-setup.html"/>
+<title>docs-url-behavior</title>
+
+<link rel="import" href="docs-url-behavior.html">
+
+<script>void(0);</script>
+
+<test-fixture id="basic">
+ <template>
+ <docs-url-behavior-element></docs-url-behavior-element>
+ </template>
+</test-fixture>
+
+<script>
+ suite('docs-url-behavior tests', () => {
+ let element;
+
+ suiteSetup(() => {
+ // Define a Polymer element that uses this behavior.
+ Polymer({
+ is: 'docs-url-behavior-element',
+ behaviors: [Gerrit.DocsUrlBehavior],
+ });
+ });
+
+ setup(() => {
+ element = fixture('basic');
+ element._clearDocsBaseUrlCache();
+ });
+
+ test('null config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('no doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.equal(docsBaseUrl, '/Documentation');
+ });
+ });
+
+ test('has doc config', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(true)),
+ };
+ const config = {gerrit: {doc_url: 'foobar'}};
+ return element.getDocsBaseUrl(config, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isFalse(mockRestApi.probePath.called);
+ assert.equal(docsBaseUrl, 'foobar');
+ });
+ });
+
+ test('no probe', () => {
+ const mockRestApi = {
+ probePath: sinon.stub().returns(Promise.resolve(false)),
+ };
+ return element.getDocsBaseUrl(null, mockRestApi)
+ .then(docsBaseUrl => {
+ assert.isTrue(
+ mockRestApi.probePath.calledWith('/Documentation/index.html'));
+ assert.isNotOk(docsBaseUrl);
+ });
+ });
+ });
+</script>
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
index 258c901..683beb6 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.html
@@ -15,6 +15,7 @@
-->
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">
diff --git a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
index 620f733..b065000 100644
--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -103,6 +103,7 @@
behaviors: [
Gerrit.BaseUrlBehavior,
+ Gerrit.DocsUrlBehavior,
],
observers: [
@@ -188,24 +189,9 @@
},
_loadConfig() {
- this.$.restAPI.getConfig().then(config => {
- if (config && config.gerrit && config.gerrit.doc_url) {
- this._docBaseUrl = config.gerrit.doc_url;
- }
- if (!this._docBaseUrl) {
- return this._probeDocLink('/Documentation/index.html');
- }
- });
- },
-
- _probeDocLink(path) {
- return this.$.restAPI.probePath(this.getBaseUrl() + path).then(ok => {
- if (ok) {
- this._docBaseUrl = this.getBaseUrl() + '/Documentation';
- } else {
- this._docBaseUrl = null;
- }
- });
+ this.$.restAPI.getConfig()
+ .then(config => this.getDocsBaseUrl(config, this.$.restAPI))
+ .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
},
_accountLoaded(account) {
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
index 0536452..96f84fd 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.html
@@ -16,6 +16,7 @@
<link rel="import" href="../../../bower_components/polymer/polymer.html">
+<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/gr-page-nav-styles.html">
@@ -44,6 +45,12 @@
#email {
margin-bottom: 1em;
}
+ .filters p {
+ margin-bottom: 1em;
+ }
+ .queryExample em {
+ color: violet;
+ }
</style>
<style include="gr-form-styles"></style>
<style include="gr-menu-page-styles"></style>
@@ -69,6 +76,7 @@
<a href="#Agreements">Agreements</a>
</li>
</template>
+ <li><a href="#MailFilters">Mail Filters</a></li>
</ul>
</gr-page-nav>
<main class="gr-form-styles">
@@ -381,6 +389,76 @@
<gr-agreements-list id="agreementsList"></gr-agreements-list>
</fieldset>
</template>
+ <h2 id="MailFilters">Mail Filters</h2>
+ <fieldset class="filters">
+ <p>
+ Gerrit emails include metadata about the change to support
+ writing mail filters.
+ </p>
+ <p>
+ Here are some example Gmail queries that can be used for filters or
+ for searching through archived messages. View the
+ <a href$="[[_getFilterDocsLink(_docsBaseUrl)]]"
+ target="_blank"
+ rel="nofollow">Gerrit documentation</a>
+ for the complete set of footers.
+ </p>
+ <table>
+ <tbody>
+ <tr><th>Name</th><th>Query</th></tr>
+ <tr>
+ <td>Changes requesting my review</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Reviewer: <em>Your Name</em>
+ <<em>your.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes from a specific owner</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Owner: <em>Owner name</em>
+ <<em>owner.email@example.com</em>>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes targeting a specific branch</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Branch: <em>branch-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Changes in a specific project</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Project: <em>project-name</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific Change ID</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Id: <em>Change ID</em>"
+ </code>
+ </td>
+ </tr>
+ <tr>
+ <td>Messages related to a specific change number</td>
+ <td>
+ <code class="queryExample">
+ "Gerrit-Change-Number: <em>change number</em>"
+ </code>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </fieldset>
</main>
</div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface>
diff --git a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
index 1e5d61a..a820403 100644
--- a/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
+++ b/polygerrit-ui/app/elements/settings/gr-settings-view/gr-settings-view.js
@@ -25,6 +25,10 @@
'email_format',
];
+ const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
+ 'Documentation';
+ const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
+
Polymer({
is: 'gr-settings-view',
@@ -103,6 +107,7 @@
value: null,
},
_serverConfig: Object,
+ _docsBaseUrl: String,
/**
* For testing purposes.
@@ -111,6 +116,7 @@
},
behaviors: [
+ Gerrit.DocsUrlBehavior,
Gerrit.ChangeTableBehavior,
],
@@ -144,9 +150,17 @@
promises.push(this.$.restAPI.getConfig().then(config => {
this._serverConfig = config;
+ const configPromises = [];
+
if (this._serverConfig.sshd) {
- return this.$.sshEditor.loadData();
+ configPromises.push(this.$.sshEditor.loadData());
}
+
+ configPromises.push(
+ this.getDocsBaseUrl(config, this.$.restAPI)
+ .then(baseUrl => { this._docsBaseUrl = baseUrl; }));
+
+ return Promise.all(configPromises);
}));
if (this.params.emailToken) {
@@ -339,5 +353,13 @@
this._newEmail = '';
});
},
+
+ _getFilterDocsLink(docsBaseUrl) {
+ let base = docsBaseUrl;
+ if (!base || !base.startsWith('http')) {
+ base = GERRIT_DOCS_BASE_URL;
+ }
+ return base + GERRIT_DOCS_FILTER_PATH;
+ },
});
})();
diff --git a/polygerrit-ui/app/test/index.html b/polygerrit-ui/app/test/index.html
index da4ad2e..f7f11b5 100644
--- a/polygerrit-ui/app/test/index.html
+++ b/polygerrit-ui/app/test/index.html
@@ -140,6 +140,7 @@
// Behaviors tests.
const behaviors = [
'base-url-behavior/base-url-behavior_test.html',
+ 'docs-url-behavior/docs-url-behavior_test.html',
'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
'rest-client-behavior/rest-client-behavior_test.html',
'gr-change-table-behavior/gr-change-table-behavior_test.html',