/
githubmirror
/
zulip
Обзор
Документация
Войти
/
githubmirror
/
zulip
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
web/src/templates.ts
329 строк
11 KB
Anders Kaseorg
dependencies: Upgrade JavaScript dependencies.
29 июл 2026, 02:35
29 июл 2026, 02:35
d257809
Код
Авторство
О чём код?
import Handlebars from "handlebars/runtime.js"; import assert from "minimalistic-assert"; import {z} from "zod/mini"; import * as blueslip from "./blueslip.ts"; import * as common from "./common.ts"; import {default_html_elements, intl} from "./i18n.ts"; import {postprocess_content} from "./postprocess_content.ts"; import {user_settings} from "./user_settings.ts"; const orig_escape_expression = Handlebars.Utils.escapeExpression; const orig_is_empty = Handlebars.Utils.isEmpty; const orig_each = Handlebars.helpers["each"]; assert(orig_each !== undefined); const orig_if = Handlebars.helpers["if"]; assert(orig_if !== undefined); const orig_unless = Handlebars.helpers["unless"]; assert(orig_unless !== undefined); Handlebars.Utils.escapeExpression = (value: unknown): string => { /* istanbul ignore if */ if ( typeof value !== "string" && typeof value !== "number" && value !== undefined && !(typeof value === "object" && value !== null && "toHTML" in value) ) { blueslip.error(`Cannot use a value of type ${typeof value} in a Zulip Handlebars template`); } // Upstream type annotation incorrectly requires string // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return (orig_escape_expression as (value: unknown) => string)(value); }; Handlebars.Utils.isEmpty = (value: unknown): boolean => { /* istanbul ignore if */ if ( typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean" && value !== undefined && (typeof value !== "object" || Array.isArray(value)) ) { blueslip.error( `Cannot test a value of type ${typeof value} in a Zulip Handlebars template`, ); } return orig_is_empty(value); }; Handlebars.helpers["each"] = function (context, options): unknown { /* istanbul ignore if */ if (!Array.isArray(context)) { blueslip.error( `Cannot loop over a value of type ${Object.prototype.toString.call(context)} in a Zulip Handlebars template`, ); } return orig_each.call(this, context, options); }; Handlebars.helpers["if"] = function (conditional, options): unknown { /* istanbul ignore if */ if (typeof conditional === "number") { blueslip.error( `Cannot test a value of type ${typeof conditional} in a Zulip Handlebars template`, ); } return orig_if.call(this, conditional, options); }; Handlebars.helpers["unless"] = function (conditional, options): unknown { /* istanbul ignore if */ if (typeof conditional === "number") { blueslip.error( `Cannot test a value of type ${typeof conditional} in a Zulip Handlebars template`, ); } return orig_unless.call(this, conditional, options); }; // Below, we register Zulip-specific extensions to the Handlebars API. // // IMPORTANT: When adding a new Handlebars helper, update the // knownHelpers array in the webpack config so that webpack knows your // helper is registered at runtime and don't try to require them when // bundling. // We don't want to wait for DOM ready to register the Handlebars helpers // below. There's no need to, as they do not access the DOM. // Furthermore, waiting for DOM ready would introduce race conditions with // other DOM-ready callbacks that attempt to render templates. Handlebars.registerHelper({ eq(a, b) { return a === b; }, and(...args: unknown[]) { args.pop(); // Handlebars options if (args.length === 0) { return true; } const last = args.pop(); for (const arg of args) { if (!arg || Handlebars.Utils.isEmpty(arg)) { return arg; } } return last; }, or(...args: unknown[]) { args.pop(); // Handlebars options if (args.length === 0) { return false; } const last = args.pop(); for (const arg of args) { if (arg && !Handlebars.Utils.isEmpty(arg)) { return arg; } } return last; }, not(a) { return !a || Handlebars.Utils.isEmpty(a); }, }); Handlebars.registerHelper("map_entries", (m: unknown) => { /* istanbul ignore if */ if (!(typeof m === "object" && m instanceof Map)) { blueslip.error("map_entries requires a Map"); return m; } return [...m]; }); Handlebars.registerHelper("object_entries", (o: unknown) => { /* istanbul ignore if */ if (typeof o !== "object" || o === null) { blueslip.error("object_entries requires a plain object"); return []; } /* istanbul ignore if */ // eslint-disable-next-line unicorn/no-computed-property-existence-check if (Symbol.iterator in o) { blueslip.error("object_entries requires a plain object"); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions return [...(o as Iterable<unknown>)].entries().toArray(); } return Object.entries(o); }); Handlebars.registerHelper("object_values", (o: unknown): unknown => { /* istanbul ignore if */ // eslint-disable-next-line unicorn/no-computed-property-existence-check if (typeof o !== "object" || o === null || Symbol.iterator in o) { blueslip.error("object_values requires a plain object"); return o; } return Object.values(o); }); type Context = Record<string, unknown>; Handlebars.registerHelper("t", function (this: Context, message: string) { // Marks a string for translation. // Example usage 1: // {{t "some English text"}} // // Example usage 2: // {{t "This {variable} will get value from the current context"}} // // Note: use `{` and `}` instead of `{{` and `}}` to declare // variables. message = message .trim() .split("\n") .map((s) => s.trim()) .join(" "); const descriptor = {id: message, defaultMessage: message}; return intl.formatMessage( descriptor, Object.fromEntries( Object.entries(this).flatMap(([key, value]) => typeof value === "string" || typeof value === "number" || value instanceof Date ? [[key, value]] : [], ), ), ); }); Handlebars.registerHelper("tr", function (this: Context, options: Handlebars.HelperOptions) { // Marks a block for translation. // Example usage 1: // {{#tr}} // <p>some English text</p> // {{/tr}} // // Example usage 2: // {{#tr}} // <p>This {variable} will get value from the current context</p> // {{/tr}} // // Note: use `{` and `}` instead of `{{` and `}}` to declare // variables. const message = options .fn(this) .trim() .split("\n") .map((s) => s.trim()) .join(" "); const descriptor = {id: message, defaultMessage: message}; const partials: Partial<Record<string, (context: Context, options: unknown) => string>> = "partials" in options.fn && typeof options.fn.partials === "object" && options.fn.partials !== null ? options.fn.partials : {}; const result = intl.formatMessage(descriptor, { ...default_html_elements, ...Object.fromEntries( Object.entries(partials).map(([name, value]) => [ name, (content_html: string[]) => value!(this, {data: {"partial-block": () => content_html.join("")}}), ]), ), ...Object.fromEntries( Object.entries(this).flatMap(([key, value]): [string, string | number | Date][] => typeof value === "string" ? [[key, Handlebars.Utils.escapeExpression(value)]] : typeof value === "number" || value instanceof Date ? [[key, value]] : [], ), ), }); return new Handlebars.SafeString(result); }); Handlebars.registerHelper( "rendered_markdown", (content: string) => new Handlebars.SafeString(postprocess_content(content)), ); Handlebars.registerHelper("numberFormat", (number: number) => number.toLocaleString()); Handlebars.registerHelper("tooltip_hotkey_hints", (...args) => { args.pop(); // Handlebars options const hotkeys: string[] = args; let hotkey_hints = ""; common.adjust_mac_hotkey_hints(hotkeys); for (const hotkey of hotkeys) { hotkey_hints += `<span class="tooltip-hotkey-hint">${hotkey}</span>`; } const result = `<span class="tooltip-hotkey-hints">${hotkey_hints}</span>`; return new Handlebars.SafeString(result); }); Handlebars.registerHelper("popover_hotkey_hints", (...args) => { args.pop(); // Handlebars options const hotkeys: string[] = args; let hotkey_hints = ""; common.adjust_mac_hotkey_hints(hotkeys); const shift_hotkey_exists = common.adjust_shift_hotkey(hotkeys); for (const hotkey of hotkeys) { // The ⌘ symbol isn't vertically centered, so we use an icon. if (hotkey === "⌘") { hotkey_hints += `<span class="popover-menu-hotkey-hint"><i class="zulip-icon zulip-icon-mac-command" aria-hidden="true"></i></span>`; } else { hotkey_hints += `<span class="popover-menu-hotkey-hint">${hotkey}</span>`; } } if (shift_hotkey_exists) { return new Handlebars.SafeString( `<span class="popover-menu-hotkey-hints popover-contains-shift-hotkey" data-hotkey-hints="${hotkeys.join(",")}">${hotkey_hints}</span>`, ); } return new Handlebars.SafeString( `<span class="popover-menu-hotkey-hints">${hotkey_hints}</span>`, ); }); const list_format_options_schema = z.object({ style: z.optional(z.enum(["narrow", "long", "short"])), type: z.optional(z.enum(["conjunction", "disjunction", "unit"])), }); Handlebars.registerHelper( "list_each", function (this: unknown, context: unknown, options: Handlebars.HelperOptions) { const {fn, inverse} = options; const items_html: string[] = []; let empty = false; assert("each" in Handlebars.helpers); const ret: unknown = Handlebars.helpers["each"].call(this, context, { ...options, fn(item_context: unknown, item_options?: Handlebars.RuntimeOptions) { items_html.push(fn(item_context, item_options)); return ""; }, inverse(item_context: unknown, item_options?: Handlebars.RuntimeOptions) { empty = true; return inverse(item_context, item_options); }, }); if (empty) { return ret; } assert.equal(ret, ""); /* istanbul ignore if */ if (Intl.ListFormat === undefined) { return items_html.join(", "); } return new Intl.ListFormat( user_settings.default_language, list_format_options_schema.parse(options.hash), ) .formatToParts(items_html) .map((part) => part.type === "element" ? part.value : Handlebars.Utils.escapeExpression(part.value), ) .join(""); }, );