Skip to content

Commit

Permalink
Mustache lambdas emiting any object including components (extended #951)
Browse files Browse the repository at this point in the history
  • Loading branch information
psrok1 committed Oct 7, 2024
1 parent 2b75f3f commit 0bd2f3d
Show file tree
Hide file tree
Showing 2 changed files with 84 additions and 74 deletions.
47 changes: 28 additions & 19 deletions mwdb/web/src/commons/helpers/renderTokens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ export type Token = {

export type Option = {
searchEndpoint: string;
lambdaResults?: { [id: string]: any };
};

// Custom renderer into React components
export function renderTokens(tokens: Token[], options?: Option): any {
export function renderTokens(tokens: Token[], options: Option): any {
const renderers = {
text(token: Token) {
return token.tokens
Expand Down Expand Up @@ -89,24 +90,32 @@ export function renderTokens(tokens: Token[], options?: Option): any {
);
},
link(token: Token) {
if (token.href && token.href.startsWith("search#")) {
const query = token.href.slice("search#".length);
const search =
"?" +
new URLSearchParams({
q: decodeURIComponent(query),
}).toString();
return (
<Link
key={uniqueId()}
to={{
pathname: options?.searchEndpoint,
search,
}}
>
{renderTokens(token.tokens ?? [], options)}
</Link>
);
if (token.href) {
if (token.href.startsWith("search#")) {
const query = token.href.slice("search#".length);
const search =
"?" +
new URLSearchParams({
q: decodeURIComponent(query),
}).toString();
return (
<Link
key={uniqueId()}
to={{
pathname: options?.searchEndpoint,
search,
}}
>
{renderTokens(token.tokens ?? [], options)}
</Link>
);
} else if (token.href.startsWith("lambda#")) {
const id = token.href.slice("lambda#".length);
if (!options.lambdaResults?.hasOwnProperty(id)) {
return <i>{`(BUG: No lambda result for ${id})`}</i>;
}
return options.lambdaResults[id];
}
}
return (
<a key={uniqueId()} href={token.href}>
Expand Down
111 changes: 56 additions & 55 deletions mwdb/web/src/components/RichAttribute/MarkedMustache.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import _ from "lodash";
import _, { uniqueId } from "lodash";
import Mustache, { Context } from "mustache";
import { marked, Tokenizer } from "marked";
import {
Expand Down Expand Up @@ -86,13 +86,22 @@ class MustacheContext extends Mustache.Context {
globalView: any;
lastPath: string[] | null;
lastValue: string | null;
constructor(view: Object, parent?: Context, globalView?: any) {
lambdaResults: { [id: string]: any };
lambdas: { [name: string]: Function };
constructor(
view: Object,
parent?: MustacheContext,
globalView?: any,
lambdas?: { [name: string]: Function }
) {
super(view, parent);
this.globalView = globalView === undefined ? view : globalView;
// Stored absolute path of last lookup
this.lastPath = null;
// Stored value from last lookup to determine the type
this.lastValue = null;
this.lambdaResults = parent ? parent.lambdaResults : {};
this.lambdas = parent ? parent.lambdas : lambdas || {};
}

push(view: Object): Context {
Expand All @@ -117,13 +126,29 @@ class MustacheContext extends Mustache.Context {
searchable = true;
}
if (!name) return undefined;

let currentObject = this.view;
if (this.lambdas[name]) {
const lambda = this.lambdas[name];
const currentContext = this;
return function lambdaFunction(
this: any,
text: string,
renderer: Function
): string {
let lambdaResultId = uniqueId("lambda_result");
let result = lambda.call(this, text, renderer);
if (typeof result !== "string") {
currentContext.lambdaResults[lambdaResultId] = result;
// Emit reference in markdown
return `[](lambda#${lambdaResultId})`;
} else {
return result;
}
};
}

const path = splitName(name);
// 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;
Expand All @@ -132,10 +157,7 @@ class MustacheContext extends Mustache.Context {
this.lastPath = this.getParentPath()!.concat(path);
this.lastValue = currentObject;
if (searchable) {
if (
isFunction(currentObject) ||
typeof currentObject === "object"
)
if (isFunction(currentObject) || typeof currentObject === "object")
// Non-primitives are not directly searchable
return undefined;
const query = makeQuery(
Expand All @@ -146,9 +168,6 @@ class MustacheContext extends Mustache.Context {
if (!query) return undefined;
return new SearchReference(query, currentObject);
}
if (isFunction(currentObject)) {
currentObject = currentObject.call(this.view);
}
return currentObject;
}
}
Expand All @@ -171,10 +190,6 @@ class MustacheWriter extends Mustache.Writer {
: escapeMarkdown(value);
return "";
}

render(template: string, view: Object) {
return super.render(template, new MustacheContext(view));
}
}

// Overrides to not use HTML escape
Expand Down Expand Up @@ -226,46 +241,32 @@ 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,
"value":
{
...(value as any).value,
...lambdas
}
}
const lambdas = fromPlugins("mustacheExtensions").reduce((prev, curr) => {
return {
...prev,
...curr,
};
}, {});
const context = new MustacheContext(
{
...value,
},
undefined,
undefined,
lambdas
);
const markdown = mustacheWriter.render(template, context);
const tokens = marked.lexer(markdown, {
...marked.defaults,
tokenizer: markedTokenizer,
}) as Token[];
return <div>{renderTokens(tokens, options)}</div>;
return (
<div>
{renderTokens(tokens, {
...options,
lambdaResults: context.lambdaResults,
})}
</div>
);
}

0 comments on commit 0bd2f3d

Please sign in to comment.