From 1abb043244838672f4fef80067eac399b3dbbec4 Mon Sep 17 00:00:00 2001 From: Sharon Yankovich <49753115+yankovs@users.noreply.github.com> Date: Sat, 6 Jul 2024 17:26:52 +0300 Subject: [PATCH] Add ability to run lambdas as plugin mustache extensions --- .../RichAttribute/MarkedMustache.tsx | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/mwdb/web/src/components/RichAttribute/MarkedMustache.tsx b/mwdb/web/src/components/RichAttribute/MarkedMustache.tsx index ac278ea6a..f3c1b2c30 100644 --- a/mwdb/web/src/components/RichAttribute/MarkedMustache.tsx +++ b/mwdb/web/src/components/RichAttribute/MarkedMustache.tsx @@ -1,3 +1,4 @@ +import _ from "lodash"; import Mustache, { Context } from "mustache"; import { marked, Tokenizer } from "marked"; import { @@ -6,6 +7,7 @@ import { renderTokens, Token, } from "@mwdb-web/commons/helpers"; +import { fromPlugins } from "@mwdb-web/commons/plugins"; /** * Markdown with Mustache templates for React @@ -24,6 +26,10 @@ function appendToLastElement(array: string[], value: string) { return [...array.slice(0, -1), array[array.length - 1] + value]; } +function isFunction(obj: Object): boolean { + return typeof obj === "function"; +} + function splitName(name: string) { if (name === ".") // Special case for "this" @@ -112,7 +118,12 @@ class MustacheContext extends Mustache.Context { } if (!name) return undefined; const path = splitName(name); - let currentObject = this.view; + // In case of lambdas, the subrenderer makes + // this.view be a MustacheContext so make sure + // we get the actual view + let currentObject = this.view instanceof MustacheContext + ? this.view.view + : this.view; for (let element of path) { if (!Object.prototype.hasOwnProperty.call(currentObject, element)) return undefined; @@ -122,8 +133,8 @@ class MustacheContext extends Mustache.Context { this.lastValue = currentObject; if (searchable) { if ( - typeof currentObject === "object" || - typeof currentObject === "function" + isFunction(currentObject) || + typeof currentObject === "object" ) // Non-primitives are not directly searchable return undefined; @@ -135,6 +146,9 @@ class MustacheContext extends Mustache.Context { if (!query) return undefined; return new SearchReference(query, currentObject); } + if (isFunction(currentObject)) { + currentObject = currentObject.call(this.view); + } return currentObject; } } @@ -212,8 +226,43 @@ class MarkedTokenizer extends Tokenizer { const mustacheWriter = new MustacheWriter(); const markedTokenizer = new MarkedTokenizer(); +type stringOpFunc = (I: string) => string; + +const lambda = (func: stringOpFunc = _.identity) => { + // A factory method for custom lambdas. + // + // The inner method receives the text inside the section + // and the subrenderer. It then renders the value, and passes + // it to a user defined function. + // + // E.g.: ("{{name}}", renderer) => "John Doe" + // (func is toUpperCase) => "JOHN DOE" + return () => function(text: string, renderer: any): string { + return func(renderer(text.trim())); + } +}; + +const lambdas = fromPlugins("mustacheExtensions").reduce( + (prev, curr) => { + return { + ...prev, + ..._.mapValues(curr, (func: stringOpFunc) => lambda(func)) + } + }, +{}); + export function renderValue(template: string, value: Object, options: Option) { - const markdown = mustacheWriter.render(template, value); + const markdown = mustacheWriter.render( + template, + { + ...value, + "value": + { + ...(value as any).value, + ...lambdas + } + } + ); const tokens = marked.lexer(markdown, { ...marked.defaults, tokenizer: markedTokenizer,