Feature: UFM component filters (#2246)

* UFM feature additions + refactor

- Re-structures "ufm-components" folder
- Adds `ufmFilter` extension type
- Adds `UmbUfmElementBase` to support reusing filters

* Code tidy-up

* Adds test for `{~contentPicker}` "ufm-content-name" syntax

* Exports `UfmPlugin` and `UfmToken` types

* Adds UFM filter extensions/functions

- Fallback
- Lowercase
- Strip HTML
- Title Case
- Truncate
- Uppercase
- Word Limit
This commit is contained in:
Lee Kelleher
2024-09-11 08:20:18 +01:00
committed by GitHub
parent f6b93de123
commit ccb9eeb08b
36 changed files with 546 additions and 191 deletions

View File

@@ -46,6 +46,7 @@ import type { ManifestTinyMcePlugin } from './tinymce-plugin.model.js';
import type { ManifestTree } from './tree.model.js';
import type { ManifestTreeItem } from './tree-item.model.js';
import type { ManifestUfmComponent } from './ufm-component.model.js';
import type { ManifestUfmFilter } from './ufm-filter.model.js';
import type { ManifestUserProfileApp } from './user-profile-app.model.js';
import type { ManifestWorkspace, ManifestWorkspaceRoutableKind } from './workspace.model.js';
import type { ManifestWorkspaceAction, ManifestWorkspaceActionDefaultKind } from './workspace-action.model.js';
@@ -117,6 +118,7 @@ export type * from './tinymce-plugin.model.js';
export type * from './tree-item.model.js';
export type * from './tree.model.js';
export type * from './ufm-component.model.js';
export type * from './ufm-filter.model.js';
export type * from './user-granular-permission.model.js';
export type * from './user-profile-app.model.js';
export type * from './workspace-action-menu-item.model.js';
@@ -210,6 +212,7 @@ export type ManifestTypes =
| ManifestTreeItem
| ManifestTreeStore
| ManifestUfmComponent
| ManifestUfmFilter
| ManifestUserProfileApp
| ManifestWorkspaceActionMenuItem
| ManifestWorkspaceActions

View File

@@ -0,0 +1,14 @@
import type { ManifestApi, UmbApi } from '@umbraco-cms/backoffice/extension-api';
export interface UmbUfmFilterApi extends UmbApi {
filter(...args: Array<unknown>): string | undefined | null;
}
export interface MetaUfmFilter {
alias: string;
}
export interface ManifestUfmFilter extends ManifestApi<UmbUfmFilterApi> {
type: 'ufmFilter';
meta: MetaUfmFilter;
}

View File

@@ -0,0 +1,15 @@
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
import { UmbUfmComponentBase } from '../ufm-component-base.js';
import './content-name.element.js';
export class UmbUfmContentNameComponent extends UmbUfmComponentBase {
render(token: UfmToken) {
if (!token.text) return;
const attributes = super.getAttributes(token.text);
return `<ufm-content-name ${attributes}></ufm-content-name>`;
}
}
export { UmbUfmContentNameComponent as api };

View File

@@ -0,0 +1,74 @@
import { UmbUfmElementBase } from '../ufm-element-base.js';
import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js';
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document';
import { UmbMediaItemRepository } from '@umbraco-cms/backoffice/media';
import { UmbMemberItemRepository } from '@umbraco-cms/backoffice/member';
const elementName = 'ufm-content-name';
@customElement(elementName)
export class UmbUfmContentNameElement extends UmbUfmElementBase {
@property()
alias?: string;
#documentRepository?: UmbDocumentItemRepository;
#mediaRepository?: UmbMediaItemRepository;
#memberRepository?: UmbMemberItemRepository;
constructor() {
super();
this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => {
this.observe(
context.value,
async (value) => {
const temp =
this.alias && typeof value === 'object'
? ((value as Record<string, unknown>)[this.alias] as string)
: (value as string);
const entityType = Array.isArray(temp) && temp.length > 0 ? temp[0].type : null;
const uniques = Array.isArray(temp) ? temp.map((x) => x.unique) : temp ? [temp] : [];
if (uniques?.length) {
const repository = this.#getRepository(entityType);
if (repository) {
const { data } = await repository.requestItems(uniques);
this.value = data ? data.map((item) => item.name).join(', ') : '';
return;
}
}
this.value = '';
},
'observeValue',
);
});
}
#getRepository(entityType?: string | null) {
switch (entityType) {
case 'media':
if (!this.#mediaRepository) this.#mediaRepository = new UmbMediaItemRepository(this);
return this.#mediaRepository;
case 'member':
if (!this.#memberRepository) this.#memberRepository = new UmbMemberItemRepository(this);
return this.#memberRepository;
case 'document':
default:
if (!this.#documentRepository) this.#documentRepository = new UmbDocumentItemRepository(this);
return this.#documentRepository;
}
}
}
export { UmbUfmContentNameElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbUfmContentNameElement;
}
}

View File

@@ -0,0 +1,3 @@
export * from './ufm-render/index.js';
export * from './ufm-component-base.js';
export * from './ufm-element-base.js';

View File

@@ -0,0 +1,15 @@
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
import { UmbUfmComponentBase } from '../ufm-component-base.js';
import './label-value.element.js';
export class UmbUfmLabelValueComponent extends UmbUfmComponentBase {
render(token: UfmToken) {
if (!token.text) return;
const attributes = super.getAttributes(token.text);
return `<ufm-label-value ${attributes}></ufm-label-value>`;
}
}
export { UmbUfmLabelValueComponent as api };

View File

@@ -1,17 +1,14 @@
import { UMB_UFM_RENDER_CONTEXT } from '../components/ufm-render/index.js';
import { customElement, nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UMB_UFM_RENDER_CONTEXT } from '../ufm-render/ufm-render.context.js';
import { UmbUfmElementBase } from '../ufm-element-base.js';
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
const elementName = 'ufm-label-value';
@customElement(elementName)
export class UmbUfmLabelValueElement extends UmbLitElement {
export class UmbUfmLabelValueElement extends UmbUfmElementBase {
@property()
alias?: string;
@state()
private _value?: unknown;
constructor() {
super();
@@ -20,19 +17,15 @@ export class UmbUfmLabelValueElement extends UmbLitElement {
context.value,
(value) => {
if (this.alias !== undefined && value !== undefined && typeof value === 'object') {
this._value = (value as Record<string, unknown>)[this.alias];
this.value = (value as Record<string, unknown>)[this.alias];
} else {
this._value = value;
this.value = value;
}
},
'observeValue',
);
});
}
override render() {
return this._value !== undefined ? this._value : nothing;
}
}
export { UmbUfmLabelValueElement as element };

View File

@@ -0,0 +1,15 @@
import type { UfmToken } from '../../plugins/marked-ufm.plugin.js';
import { UmbUfmComponentBase } from '../ufm-component-base.js';
import './localize.element.js';
export class UmbUfmLocalizeComponent extends UmbUfmComponentBase {
render(token: UfmToken) {
if (!token.text) return;
const attributes = super.getAttributes(token.text);
return `<ufm-localize ${attributes}></ufm-localize>`;
}
}
export { UmbUfmLocalizeComponent as api };

View File

@@ -0,0 +1,26 @@
import { UmbUfmElementBase } from '../ufm-element-base.js';
import { customElement, property } from '@umbraco-cms/backoffice/external/lit';
const elementName = 'ufm-localize';
@customElement(elementName)
export class UmbUfmLocalizeElement extends UmbUfmElementBase {
@property()
public set alias(value: string | undefined) {
if (!value) return;
this.#alias = value;
this.value = this.localize.term(value);
}
public get alias(): string | undefined {
return this.#alias;
}
#alias?: string;
}
export { UmbUfmLocalizeElement as element };
declare global {
interface HTMLElementTagNameMap {
[elementName]: UmbUfmLocalizeElement;
}
}

View File

@@ -0,0 +1,25 @@
import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestUfmComponent> = [
{
type: 'ufmComponent',
alias: 'Umb.Markdown.LabelValue',
name: 'Label Value UFM Component',
api: () => import('./label-value/label-value.component.js'),
meta: { marker: '=' },
},
{
type: 'ufmComponent',
alias: 'Umb.Markdown.Localize',
name: 'Localize UFM Component',
api: () => import('./localize/localize.component.js'),
meta: { marker: '#' },
},
{
type: 'ufmComponent',
alias: 'Umb.Markdown.ContentName',
name: 'Content Name UFM Component',
api: () => import('./content-name/content-name.component.js'),
meta: { marker: '~' },
},
];

View File

@@ -0,0 +1,25 @@
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry';
export abstract class UmbUfmComponentBase implements UmbUfmComponentApi {
protected getAttributes(text: string): string | null {
if (!text) return null;
const pipeIndex = text.indexOf('|');
if (pipeIndex === -1) {
return `alias="${text.trim()}"`;
}
const alias = text.substring(0, pipeIndex).trim();
const filters = text.substring(pipeIndex + 1).trim();
return Object.entries({ alias, filters })
.map(([key, value]) => (value ? `${key}="${value.trim()}"` : null))
.join(' ');
}
abstract render(token: UfmToken): string | undefined;
destroy() {}
}

View File

@@ -0,0 +1,54 @@
import { UMB_UFM_CONTEXT } from '../contexts/ufm.context.js';
import { nothing, property, state } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
// eslint-disable-next-line local-rules/enforce-element-suffix-on-element-class-name
export abstract class UmbUfmElementBase extends UmbLitElement {
#filterFuncArgs?: Array<{ alias: string; args: Array<string> }>;
@property()
public set filters(value: string | undefined) {
this.#filters = value;
this.#filterFuncArgs = value
?.split('|')
.filter((item) => item)
.map((item) => {
const [alias, ...args] = item.split(':').map((x) => x.trim());
return { alias, args };
});
}
public get filters(): string | undefined {
return this.#filters;
}
#filters?: string;
@state()
value?: unknown;
#ufmContext?: typeof UMB_UFM_CONTEXT.TYPE;
constructor() {
super();
this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => {
this.#ufmContext = ufmContext;
});
}
override render() {
if (!this.#ufmContext) return nothing;
let values = Array.isArray(this.value) ? this.value : [this.value];
if (this.#filterFuncArgs) {
for (const item of this.#filterFuncArgs) {
const filter = this.#ufmContext.getFilterByAlias(item.alias);
if (filter) {
values = values.map((value) => filter(value, ...item.args));
}
}
}
return values.join(', ');
}
}

View File

@@ -1,7 +1,7 @@
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import { UmbObjectState } from '@umbraco-cms/backoffice/observable-api';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
export class UmbUfmRenderContext extends UmbContextBase<UmbUfmRenderContext> {
#value = new UmbObjectState<unknown>(undefined);

View File

@@ -1,43 +1,8 @@
import type { UfmPlugin } from '../../plugins/marked-ufm.plugin.js';
import { ufm } from '../../plugins/marked-ufm.plugin.js';
import { UMB_UFM_CONTEXT } from '../../contexts/ufm.context.js';
import { UmbUfmRenderContext } from './ufm-render.context.js';
import { css, customElement, nothing, property, unsafeHTML, until } from '@umbraco-cms/backoffice/external/lit';
import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify';
import { Marked } from '@umbraco-cms/backoffice/external/marked';
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
const UmbDomPurify = DOMPurify(window);
const UmbDomPurifyConfig: DOMPurify.Config = {
USE_PROFILES: { html: true },
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^(?:ufm|umb|uui)-.*$/,
attributeNameCheck: /.+/,
allowCustomizedBuiltInElements: false,
},
};
UmbDomPurify.addHook('afterSanitizeAttributes', function (node) {
// set all elements owning target to target=_blank
if ('target' in node) {
node.setAttribute('target', '_blank');
}
});
export const UmbMarked = new Marked({
async: true,
gfm: true,
breaks: true,
hooks: {
postprocess: (markup) => {
return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string;
},
},
});
const elementName = 'umb-ufm-render';
@@ -59,26 +24,13 @@ export class UmbUfmRenderElement extends UmbLitElement {
return this.#context.getValue();
}
#ufmContext?: typeof UMB_UFM_CONTEXT.TYPE;
constructor() {
super();
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => {
UmbMarked.use(
ufm(
controllers
.map((controller) => {
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmComponent>;
if (!ctrl.manifest || !ctrl.api) return;
return {
alias: ctrl.manifest.alias,
marker: ctrl.manifest.meta.marker,
render: ctrl.api.render,
};
})
.filter((x) => x) as Array<UfmPlugin>,
),
);
this.requestUpdate('markdown');
this.consumeContext(UMB_UFM_CONTEXT, (ufmContext) => {
this.#ufmContext = ufmContext;
});
}
@@ -87,8 +39,8 @@ export class UmbUfmRenderElement extends UmbLitElement {
}
async #renderMarkdown() {
if (!this.markdown) return null;
const markup = !this.inline ? await UmbMarked.parse(this.markdown) : await UmbMarked.parseInline(this.markdown);
if (!this.#ufmContext || !this.markdown) return null;
const markup = await this.#ufmContext.parse(this.markdown, this.inline);
return markup ? unsafeHTML(markup) : nothing;
}

View File

@@ -0,0 +1 @@
export * from './ufm.context.js';

View File

@@ -0,0 +1,8 @@
import type { ManifestGlobalContext } from '@umbraco-cms/backoffice/extension-registry';
export const manifest: ManifestGlobalContext = {
type: 'globalContext',
alias: 'Umb.GlobalContext.Ufm',
name: 'UFM Context',
api: () => import('./ufm.context.js'),
};

View File

@@ -0,0 +1,96 @@
import { ufm } from '../plugins/marked-ufm.plugin.js';
import type { UfmPlugin } from '../plugins/marked-ufm.plugin.js';
import { DOMPurify } from '@umbraco-cms/backoffice/external/dompurify';
import { Marked } from '@umbraco-cms/backoffice/external/marked';
import { UmbArrayState } from '@umbraco-cms/backoffice/observable-api';
import { UmbContextBase } from '@umbraco-cms/backoffice/class-api';
import { UmbContextToken } from '@umbraco-cms/backoffice/context-api';
import { UmbExtensionsApiInitializer } from '@umbraco-cms/backoffice/extension-api';
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
import type { ManifestUfmFilter, ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
import type { UmbControllerHost } from '@umbraco-cms/backoffice/controller-api';
import type { UmbExtensionApiInitializer } from '@umbraco-cms/backoffice/extension-api';
const UmbDomPurify = DOMPurify(window);
const UmbDomPurifyConfig: DOMPurify.Config = {
USE_PROFILES: { html: true },
CUSTOM_ELEMENT_HANDLING: {
tagNameCheck: /^(?:ufm|umb|uui)-.*$/,
attributeNameCheck: /.+/,
allowCustomizedBuiltInElements: false,
},
};
UmbDomPurify.addHook('afterSanitizeAttributes', function (node) {
// set all elements owning target to target=_blank
if ('target' in node) {
node.setAttribute('target', '_blank');
}
});
export const UmbMarked = new Marked({
async: true,
gfm: true,
breaks: true,
hooks: {
postprocess: (markup) => {
return UmbDomPurify.sanitize(markup, UmbDomPurifyConfig) as string;
},
},
});
type UmbUfmFilterType = {
alias: string;
filter: ((...args: Array<unknown>) => string | undefined | null) | undefined;
};
export class UmbUfmContext extends UmbContextBase<UmbUfmContext> {
#filters = new UmbArrayState<UmbUfmFilterType>([], (x) => x.alias);
public readonly filters = this.#filters.asObservable();
constructor(host: UmbControllerHost) {
super(host, UMB_UFM_CONTEXT);
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmComponent', [], undefined, (controllers) => {
UmbMarked.use(
ufm(
controllers
.map((controller) => {
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmComponent>;
if (!ctrl.manifest || !ctrl.api) return;
return {
alias: ctrl.manifest.alias,
marker: ctrl.manifest.meta.marker,
render: ctrl.api.render,
};
})
.filter((x) => x) as Array<UfmPlugin>,
),
);
});
new UmbExtensionsApiInitializer(this, umbExtensionsRegistry, 'ufmFilter', [], undefined, (controllers) => {
const filters = controllers
.map((controller) => {
const ctrl = controller as unknown as UmbExtensionApiInitializer<ManifestUfmFilter>;
if (!ctrl.manifest || !ctrl.api) return null;
return { alias: ctrl.manifest.meta.alias, filter: ctrl.api.filter };
})
.filter((x) => x) as Array<UmbUfmFilterType>;
this.#filters.setValue(filters);
});
}
public getFilterByAlias(alias: string) {
return this.#filters.getValue().find((x) => x.alias === alias)?.filter;
}
public async parse(markdown: string, inline: boolean) {
return !inline ? await UmbMarked.parse(markdown) : await UmbMarked.parseInline(markdown);
}
}
export const UMB_UFM_CONTEXT = new UmbContextToken<UmbUfmContext>('UmbUfmContext');
export { UmbUfmContext as api };

View File

@@ -0,0 +1,9 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmFallbackFilterApi extends UmbUfmFilterBase {
filter(str: string, fallback: string) {
return typeof str !== 'string' || str ? str : fallback;
}
}
export { UmbUfmFallbackFilterApi as api };

View File

@@ -0,0 +1,9 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmLowercaseFilterApi extends UmbUfmFilterBase {
filter(str?: string) {
return str?.toLocaleLowerCase();
}
}
export { UmbUfmLowercaseFilterApi as api };

View File

@@ -0,0 +1,53 @@
import type { ManifestUfmFilter } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestUfmFilter> = [
{
type: 'ufmFilter',
alias: 'Umb.Filter.Fallback',
name: 'Fallback UFM Filter',
api: () => import('./fallback.filter.js'),
meta: { alias: 'fallback' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.Lowercase',
name: 'Lowercase UFM Filter',
api: () => import('./lowercase.filter.js'),
meta: { alias: 'lowercase' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.StripHtml',
name: 'Strip HTML UFM Filter',
api: () => import('./strip-html.filter.js'),
meta: { alias: 'strip-html' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.TitleCase',
name: 'Title Case UFM Filter',
api: () => import('./title-case.filter.js'),
meta: { alias: 'title-case' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.Truncate',
name: 'Truncate UFM Filter',
api: () => import('./truncate.filter.js'),
meta: { alias: 'truncate' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.Uppercase',
name: 'Uppercase UFM Filter',
api: () => import('./uppercase.filter.js'),
meta: { alias: 'uppercase' },
},
{
type: 'ufmFilter',
alias: 'Umb.Filter.WordLimit',
name: 'Word Limit UFM Filter',
api: () => import('./word-limit.filter.js'),
meta: { alias: 'word-limit' },
},
];

View File

@@ -0,0 +1,15 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmStripHtmlFilterApi extends UmbUfmFilterBase {
filter(value: string | { markup: string } | undefined | null) {
if (!value) return '';
const markup = typeof value === 'object' && Object.hasOwn(value, 'markup') ? value.markup : (value as string);
const parser = new DOMParser();
const doc = parser.parseFromString(markup, 'text/html');
return doc.body.textContent ?? '';
}
}
export { UmbUfmStripHtmlFilterApi as api };

View File

@@ -0,0 +1,9 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmTitleCaseFilterApi extends UmbUfmFilterBase {
filter(str?: string) {
return str?.replace(/\w\S*/g, (txt) => txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase());
}
}
export { UmbUfmTitleCaseFilterApi as api };

View File

@@ -0,0 +1,14 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmTruncateFilterApi extends UmbUfmFilterBase {
filter(str: string, length: number, tail?: string) {
if (typeof str !== 'string' || !str.length) return str;
if (tail === 'false') tail = '';
if (tail === 'true') tail = '…';
tail = !tail && tail !== '' ? '…' : tail;
return str.slice(0, length).trim() + tail;
}
}
export { UmbUfmTruncateFilterApi as api };

View File

@@ -0,0 +1,9 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmUppercaseFilterApi extends UmbUfmFilterBase {
filter(str?: string) {
return str?.toLocaleUpperCase();
}
}
export { UmbUfmUppercaseFilterApi as api };

View File

@@ -0,0 +1,10 @@
import { UmbUfmFilterBase } from '../types.js';
class UmbUfmWordLimitFilterApi extends UmbUfmFilterBase {
filter(str: string, limit: number) {
const words = str?.split(/\s+/) ?? [];
return limit && words.length > limit ? words.slice(0, limit).join(' ') : str;
}
}
export { UmbUfmWordLimitFilterApi as api };

View File

@@ -1,3 +1,4 @@
export * from './components/ufm-render/index.js';
export * from './plugins/marked-ufm.plugin.js';
export * from './ufm-components/ufm-component-base.js';
export * from './types.js';
export * from './components/index.js';
export * from './contexts/index.js';
export * from './plugins/index.js';

View File

@@ -1,18 +1,6 @@
import type { ManifestUfmComponent } from '@umbraco-cms/backoffice/extension-registry';
import { manifest as ufmContext } from './contexts/manifest.js';
import { manifests as ufmComponents } from './components/manifests.js';
import { manifests as ufmFilters } from './filters/manifests.js';
import type { ManifestTypes } from '@umbraco-cms/backoffice/extension-registry';
export const manifests: Array<ManifestUfmComponent> = [
{
type: 'ufmComponent',
alias: 'Umb.Markdown.LabelValue',
name: 'Label Value UFM Component',
api: () => import('./ufm-components/label-value.component.js'),
meta: { marker: '=' },
},
{
type: 'ufmComponent',
alias: 'Umb.Markdown.Localize',
name: 'Localize UFM Component',
api: () => import('./ufm-components/localize.component.js'),
meta: { marker: '#' },
},
];
export const manifests: Array<ManifestTypes> = [ufmContext, ...ufmComponents, ...ufmFilters];

View File

@@ -0,0 +1,2 @@
export { ufm } from './marked-ufm.plugin.js';
export type { UfmPlugin, UfmToken } from './marked-ufm.plugin.js';

View File

@@ -12,7 +12,8 @@ export interface UfmToken extends Tokens.Generic {
/**
*
* @param plugins
* @param {Array<UfmPlugin>} plugins - An array of UFM plugins.
* @returns {MarkedExtension} A Marked extension object.
*/
export function ufm(plugins: Array<UfmPlugin> = []): MarkedExtension {
return {

View File

@@ -1,8 +1,9 @@
import { expect } from '@open-wc/testing';
import { ufm } from './marked-ufm.plugin.js';
import { UmbMarked } from '../index.js';
import { UmbUfmLabelValueComponent } from '../ufm-components/label-value.component.js';
import { UmbUfmLocalizeComponent } from '../ufm-components/localize.component.js';
import { UmbMarked } from '../contexts/ufm.context.js';
import { UmbUfmContentNameComponent } from '../components/content-name/content-name.component.js';
import { UmbUfmLabelValueComponent } from '../components/label-value/label-value.component.js';
import { UmbUfmLocalizeComponent } from '../components/localize/localize.component.js';
describe('UmbMarkedUfm', () => {
describe('UFM parsing', () => {
@@ -11,12 +12,18 @@ describe('UmbMarkedUfm', () => {
{ ufm: '{= prop1}', expected: '<ufm-label-value alias="prop1"></ufm-label-value>' },
{ ufm: '{= prop1 }', expected: '<ufm-label-value alias="prop1"></ufm-label-value>' },
{ ufm: '{{=prop1}}', expected: '{<ufm-label-value alias="prop1"></ufm-label-value>}' },
{ ufm: '{#general_add}', expected: '<umb-localize key="general_add"></umb-localize>' },
{
ufm: '{= prop1 | strip-html | truncate:30}',
expected: '<ufm-label-value filters="strip-html | truncate:30" alias="prop1"></ufm-label-value>',
},
{ ufm: '{#general_add}', expected: '<ufm-localize alias="general_add"></ufm-localize>' },
{ ufm: '{~contentPicker}', expected: '<ufm-content-name alias="contentPicker"></ufm-content-name>' },
];
// Manually configuring the UFM components for testing.
UmbMarked.use(
ufm([
{ alias: 'Umb.Markdown.ContentName', marker: '~', render: new UmbUfmContentNameComponent().render },
{ alias: 'Umb.Markdown.LabelValue', marker: '=', render: new UmbUfmLabelValueComponent().render },
{ alias: 'Umb.Markdown.Localize', marker: '#', render: new UmbUfmLocalizeComponent().render },
]),

View File

@@ -0,0 +1,6 @@
import type { UmbUfmFilterApi } from '@umbraco-cms/backoffice/extension-registry';
export abstract class UmbUfmFilterBase implements UmbUfmFilterApi {
abstract filter(...args: Array<unknown>): string | undefined | null;
destroy() {}
}

View File

@@ -1,12 +0,0 @@
// import { UmbUfmComponentBase } from './ufm-component-base.js';
// import type { Tokens } from '@umbraco-cms/backoffice/external/marked';
// import './document-name.element.js';
// export class UmbUfmDocumentNameComponent extends UmbUfmComponentBase {
// render(token: Tokens.Generic) {
// return `<ufm-document-name alias="${token.text}" debug></ufm-document-name>`;
// }
// }
// export { UmbUfmDocumentNameComponent as api };

View File

@@ -1,54 +0,0 @@
// import { UMB_UFM_RENDER_CONTEXT } from '@umbraco-cms/backoffice/components';
// import { UmbDocumentItemRepository } from '@umbraco-cms/backoffice/document';
// import { customElement, property, state } from '@umbraco-cms/backoffice/external/lit';
// import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
// const elementName = 'ufm-document-name';
// @customElement(elementName)
// export class UmbUfmDocumentNameElement extends UmbLitElement {
// @property()
// alias?: string;
// @state()
// private _value?: unknown;
// #documentRepository = new UmbDocumentItemRepository(this);
// constructor() {
// super();
// this.consumeContext(UMB_UFM_RENDER_CONTEXT, (context) => {
// this.observe(
// context.value,
// async (value) => {
// if (!value) return;
// const unique =
// this.alias && typeof value === 'object'
// ? ((value as Record<string, unknown>)[this.alias] as string)
// : (value as string);
// if (!unique) return;
// const { data } = await this.#documentRepository.requestItems([unique]);
// this._value = data?.[0]?.name;
// },
// 'observeValue',
// );
// });
// }
// override render() {
// return this._value ?? this.alias;
// }
// }
// export { UmbUfmDocumentNameElement as element };
// declare global {
// interface HTMLElementTagNameMap {
// [elementName]: UmbUfmDocumentNameElement;
// }
// }

View File

@@ -1,13 +0,0 @@
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
import { UmbUfmComponentBase } from './ufm-component-base.js';
import './label-value.element.js';
export class UmbUfmLabelValueComponent extends UmbUfmComponentBase {
render(token: UfmToken) {
if (!token.text) return;
return `<ufm-label-value alias="${token.text}"></ufm-label-value>`;
}
}
export { UmbUfmLabelValueComponent as api };

View File

@@ -1,11 +0,0 @@
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
import { UmbUfmComponentBase } from './ufm-component-base.js';
export class UmbUfmLocalizeComponent extends UmbUfmComponentBase {
render(token: UfmToken) {
if (!token.text) return;
return `<umb-localize key="${token.text}"></umb-localize>`;
}
}
export { UmbUfmLocalizeComponent as api };

View File

@@ -1,7 +0,0 @@
import type { UfmToken } from '../plugins/marked-ufm.plugin.js';
import type { UmbUfmComponentApi } from '@umbraco-cms/backoffice/extension-registry';
export abstract class UmbUfmComponentBase implements UmbUfmComponentApi {
abstract render(token: UfmToken): string | undefined;
destroy() {}
}