Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/Umbraco.Web.UI.Client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/Umbraco.Web.UI.Client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"./block-rte": "./dist-cms/packages/block/block-rte/index.js",
"./block-type": "./dist-cms/packages/block/block-type/index.js",
"./block": "./dist-cms/packages/block/block/index.js",
"./cache": "./dist-cms/packages/core/cache/index.js",
"./clipboard": "./dist-cms/packages/clipboard/index.js",
"./code-editor": "./dist-cms/packages/code-editor/index.js",
"./collection": "./dist-cms/packages/core/collection/index.js",
Expand Down Expand Up @@ -119,6 +120,7 @@
"./workspace": "./dist-cms/packages/core/workspace/index.js",
"./external/backend-api": "./dist-cms/packages/core/backend-api/index.js",
"./external/dompurify": "./dist-cms/external/dompurify/index.js",
"./external/heximal-expressions": "./dist-cms/external/heximal-expressions/index.js",
"./external/lit": "./dist-cms/external/lit/index.js",
"./external/marked": "./dist-cms/external/marked/index.js",
"./external/monaco-editor": "./dist-cms/external/monaco-editor/index.js",
Expand Down Expand Up @@ -200,6 +202,7 @@
"npm": ">=10.9"
},
"dependencies": {
"@heximal/expressions": "^0.1.5",
"@tiptap/core": "2.11.7",
"@tiptap/extension-character-count": "2.11.7",
"@tiptap/extension-image": "2.11.7",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '@heximal/expressions';
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/cache/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './lru-cache.js';
40 changes: 40 additions & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/cache/lru-cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* @description
* This class provides a Least Recently Used (LRU) cache implementation.
* It is designed to store key-value pairs and automatically remove the least recently used items when the cache exceeds a maximum size.
*/
export class UmbLruCache<K, V> {
#cache = new Map<K, V>();

#maxSize: number;

constructor(maxSize: number) {
this.#maxSize = maxSize;
}

get(key: K): V | undefined {
if (!this.#cache.has(key)) return undefined;
const value = this.#cache.get(key)!;
this.#cache.delete(key);
this.#cache.set(key, value);
return value;
}

set(key: K, value: V): void {
if (this.#cache.has(key)) {
this.#cache.delete(key);
} else if (this.#cache.size >= this.#maxSize) {
const oldestKey = this.#cache.keys().next().value;
if (oldestKey) {
this.#cache.delete(oldestKey);
}
}
this.#cache.set(key, value);
}

has(key: K): boolean {
return this.#cache.has(key);
}
}

export default UmbLruCache;
1 change: 1 addition & 0 deletions src/Umbraco.Web.UI.Client/src/packages/core/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export default defineConfig({
'audit-log/index': './audit-log/index.ts',
'auth/index': './auth/index.ts',
'backend-api/index': './backend-api/index.ts',
'cache/index': './cache/index.ts',
'collection/index': './collection/index.ts',
'components/index': './components/index.ts',
'const/index': './const/index.ts',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './ufm-render/index.js';
export * from './ufm-component-base.js';
export * from './ufm-element-base.js';
export * from './ufm-js-expression.element.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { UMB_UFM_CONTEXT } from '../contexts/ufm.context.js';
import { UMB_UFM_RENDER_CONTEXT } from './ufm-render/ufm-render.context.js';
import { customElement, state } from '@umbraco-cms/backoffice/external/lit';
import { EvalAstFactory, Parser } from '@umbraco-cms/backoffice/external/heximal-expressions';
import { UmbLruCache } from '@umbraco-cms/backoffice/cache';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import type { Expression, Scope } from '@umbraco-cms/backoffice/external/heximal-expressions';

const astFactory = new EvalAstFactory();
const expressionCache = new UmbLruCache<string, Expression | undefined>(1000);

@customElement('umb-ufm-js-expression')
export class UmbUfmJsExpressionElement extends UmbLitElement {
#ufmContext?: typeof UMB_UFM_CONTEXT.TYPE;

@state()
value?: unknown;

constructor() {
super();

this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => {
this.#ufmContext = ufmContext;
});

this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => {
this.observe(
context?.value,
(value) => {
this.value = this.#labelTemplate(this.textContent ?? '', value);
},
'observeValue',
);
});
}

#labelTemplate(expression: string, model?: any): string {
const filters = this.#ufmContext?.getFilters() ?? [];
const functions = Object.fromEntries(filters.map((x) => [x.alias, x.filter]));
const scope: Scope = { ...model, ...functions };

let ast = expressionCache.get(expression);

if (ast === undefined && !expressionCache.has(expression)) {
try {
ast = new Parser(expression, astFactory).parse();
} catch {
console.error(`Error parsing expression: \`${expression}\``);
}
expressionCache.set(expression, ast);
}

return ast?.evaluate(scope) ?? '';
}

override render() {
return (Array.isArray(this.value) ? this.value : [this.value]).join(', ');
}
}

export default UmbUfmJsExpressionElement;

declare global {
interface HTMLElementTagNameMap {
'umb-ufm-js-expression': UmbUfmJsExpressionElement;
}
}
29 changes: 25 additions & 4 deletions src/Umbraco.Web.UI.Client/src/packages/ufm/contexts/ufm.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,11 @@ export const UmbMarked = new Marked({
},
});

type UmbUfmFilterType = {
export type UmbUfmFilterFunction = ((...args: Array<unknown>) => string | undefined | null) | undefined;

export type UmbUfmFilterType = {
alias: string;
filter: ((...args: Array<unknown>) => string | undefined | null) | undefined;
filter: UmbUfmFilterFunction;
};

export class UmbUfmContext extends UmbContextBase {
Expand All @@ -63,11 +65,30 @@ export class UmbUfmContext extends UmbContextBase {
});
}

public getFilterByAlias(alias: string) {
/**
* Get the filters registered in the UFM context.
* @returns {Array<UmbUfmFilterType>} An array of filters with their aliases and filter functions.
*/
public getFilters(): Array<UmbUfmFilterType> {
return this.#filters.getValue();
}

/**
* Get a filter by its alias.
* @param alias The alias of the filter to retrieve.
* @returns {UmbUfmFilterFunction} The filter function associated with the alias, or undefined if not found.
*/
public getFilterByAlias(alias: string): UmbUfmFilterFunction {
return this.#filters.getValue().find((x) => x.alias === alias)?.filter;
}

public async parse(markdown: string, inline: boolean) {
/**
* Parse markdown content, optionally inline.
* @param markdown The markdown string to parse.
* @param inline If true, parse inline markdown; otherwise, parse block markdown.
* @returns {Promise<string>} A promise that resolves to the parsed HTML string.
*/
public async parse(markdown: string, inline: boolean): Promise<string> {
return !inline ? await UmbMarked.parse(markdown) : await UmbMarked.parseInline(markdown);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ export const manifests: Array<ManifestMarkedExtension> = [
alias: 'ufm',
},
},
{
type: 'markedExtension',
alias: 'Umb.MarkedExtension.Ufmjs',
name: 'UFM JS Marked Extension',
api: () => import('./ufmjs-marked-extension.api.js'),
meta: {
alias: 'ufmjs',
},
},
];
11 changes: 6 additions & 5 deletions src/Umbraco.Web.UI.Client/src/packages/ufm/extensions/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export type * from './marked-extension.extension.js';
export type * from './ufm-filter.extension.js';
export type * from './ufm-component.extension.js';

export type { UmbUfmMarkedExtensionApi } from './ufm-marked-extension.api.js';
export type * from './marked-extension.extension.js';
export type * from './ufm-filter.extension.js';
export type * from './ufm-component.extension.js';

export type { UmbUfmMarkedExtensionApi } from './ufm-marked-extension.api.js';
export type { UmbUfmJsMarkedExtensionApi } from './ufmjs-marked-extension.api.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ufmjs } from '../plugins/marked-ufmjs.plugin.js';
import type { UmbMarkedExtensionApi } from './marked-extension.extension.js';
import type { Marked } from '@umbraco-cms/backoffice/external/marked';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';

export class UmbUfmJsMarkedExtensionApi implements UmbMarkedExtensionApi {
constructor(_host: UmbControllerHost, marked: Marked) {
marked.use(ufmjs());
}

destroy() {}
}

export default UmbUfmJsMarkedExtensionApi;
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { ufm } from './marked-ufm.plugin.js';
export { ufmjs } from './marked-ufmjs.plugin.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { MarkedExtension, Tokens } from '@umbraco-cms/backoffice/external/marked';

/**
* @returns {MarkedExtension} A Marked extension object.
*/
export function ufmjs(): MarkedExtension {
return {
extensions: [
{
name: 'ufmjs',
level: 'inline',
start: (src: string) => src.search(/(?<!\\)\$\{/),
tokenizer: (src: string) => {
const pattern = /^\$\{((?:[^{}]|\{[^{}]*\})*)\}/;
const regex = new RegExp(pattern);
const match = src.match(regex);

if (match) {
const [raw, text] = match;
return {
type: 'ufmjs',
raw: raw,
tokens: [],
text: text.trim(),
};
}

return undefined;
},
renderer: (token: Tokens.Generic) => {
return `<umb-ufm-js-expression>${token.text}</umb-ufm-js-expression>`;
},
},
],
};
}
2 changes: 2 additions & 0 deletions src/Umbraco.Web.UI.Client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/block-rte": ["./src/packages/block/block-rte/index.ts"],
"@umbraco-cms/backoffice/block-type": ["./src/packages/block/block-type/index.ts"],
"@umbraco-cms/backoffice/block": ["./src/packages/block/block/index.ts"],
"@umbraco-cms/backoffice/cache": ["./src/packages/core/cache/index.ts"],
"@umbraco-cms/backoffice/clipboard": ["./src/packages/clipboard/index.ts"],
"@umbraco-cms/backoffice/code-editor": ["./src/packages/code-editor/index.ts"],
"@umbraco-cms/backoffice/collection": ["./src/packages/core/collection/index.ts"],
Expand Down Expand Up @@ -148,6 +149,7 @@ DON'T EDIT THIS FILE DIRECTLY. It is generated by /devops/tsconfig/index.js
"@umbraco-cms/backoffice/workspace": ["./src/packages/core/workspace/index.ts"],
"@umbraco-cms/backoffice/external/backend-api": ["./src/packages/core/backend-api/index.ts"],
"@umbraco-cms/backoffice/external/dompurify": ["./src/external/dompurify/index.ts"],
"@umbraco-cms/backoffice/external/heximal-expressions": ["./src/external/heximal-expressions/index.ts"],
"@umbraco-cms/backoffice/external/lit": ["./src/external/lit/index.ts"],
"@umbraco-cms/backoffice/external/marked": ["./src/external/marked/index.ts"],
"@umbraco-cms/backoffice/external/monaco-editor": ["./src/external/monaco-editor/index.ts"],
Expand Down
Loading