@emkodev/emroute 1.0.3 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +147 -12
- package/package.json +48 -7
- package/runtime/abstract.runtime.ts +441 -0
- package/runtime/bun/esbuild-runtime-loader.plugin.ts +94 -0
- package/runtime/bun/fs/bun-fs.runtime.ts +245 -0
- package/runtime/bun/sqlite/bun-sqlite.runtime.ts +279 -0
- package/runtime/sitemap.generator.ts +180 -0
- package/server/codegen.util.ts +66 -0
- package/server/emroute.server.ts +398 -0
- package/server/esbuild-manifest.plugin.ts +243 -0
- package/server/scanner.util.ts +243 -0
- package/server/server-api.type.ts +90 -0
- package/src/component/abstract.component.ts +229 -0
- package/src/component/page.component.ts +134 -0
- package/src/component/widget.component.ts +85 -0
- package/src/element/component.element.ts +353 -0
- package/src/element/markdown.element.ts +107 -0
- package/src/element/slot.element.ts +31 -0
- package/src/index.ts +61 -0
- package/src/overlay/mod.ts +10 -0
- package/src/overlay/overlay.css.ts +170 -0
- package/src/overlay/overlay.service.ts +348 -0
- package/src/overlay/overlay.type.ts +38 -0
- package/src/renderer/spa/base.renderer.ts +186 -0
- package/src/renderer/spa/hash.renderer.ts +215 -0
- package/src/renderer/spa/html.renderer.ts +382 -0
- package/src/renderer/spa/mod.ts +76 -0
- package/src/renderer/ssr/html.renderer.ts +159 -0
- package/src/renderer/ssr/md.renderer.ts +142 -0
- package/src/renderer/ssr/ssr.renderer.ts +286 -0
- package/src/route/route.core.ts +316 -0
- package/src/route/route.matcher.ts +260 -0
- package/src/type/logger.type.ts +24 -0
- package/src/type/markdown.type.ts +21 -0
- package/src/type/navigation-api.d.ts +95 -0
- package/src/type/route.type.ts +149 -0
- package/src/type/widget.type.ts +65 -0
- package/src/util/html.util.ts +186 -0
- package/src/util/logger.util.ts +83 -0
- package/src/util/widget-resolve.util.ts +197 -0
- package/src/web-doc/index.md +15 -0
- package/src/widget/breadcrumb.widget.ts +106 -0
- package/src/widget/page-title.widget.ts +52 -0
- package/src/widget/widget.parser.ts +89 -0
- package/src/widget/widget.registry.ts +51 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Component Architecture
|
|
3
|
+
*
|
|
4
|
+
* Everything is a Component: pages and widgets.
|
|
5
|
+
* Components render differently based on context:
|
|
6
|
+
* - /md/* → Markdown (LLMs, text clients)
|
|
7
|
+
* - /html/* → Pre-rendered HTML (SSR)
|
|
8
|
+
* - SPA → Hydrated custom elements
|
|
9
|
+
*
|
|
10
|
+
* Precedence (like .ts/.html/.md):
|
|
11
|
+
* - renderHTML() if defined → full HTML control
|
|
12
|
+
* - renderMarkdown() → converted to HTML via markdown renderer
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { RouteInfo } from '../type/route.type.ts';
|
|
16
|
+
import { escapeHtml } from '../util/html.util.ts';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Context passed to components during rendering.
|
|
20
|
+
* Extends RouteInfo (pathname, pattern, params, searchParams)
|
|
21
|
+
* with pre-loaded file content and an abort signal.
|
|
22
|
+
*
|
|
23
|
+
* Consumers can extend this interface via module augmentation
|
|
24
|
+
* to add app-level services (RPC clients, auth, feature flags, etc.).
|
|
25
|
+
*/
|
|
26
|
+
/** Shape of companion file contents (html, md, css). Used by generated `.page.files.g.ts` modules. */
|
|
27
|
+
export type FileContents = { html?: string; md?: string; css?: string };
|
|
28
|
+
|
|
29
|
+
export interface ComponentContext extends RouteInfo {
|
|
30
|
+
readonly files?: Readonly<FileContents>;
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
/** True when this component is the leaf (matched) route, false when rendered as a layout parent. */
|
|
33
|
+
readonly isLeaf?: boolean;
|
|
34
|
+
/** Base path for SSR HTML links (e.g. '/html'). */
|
|
35
|
+
readonly basePath?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Callback that enriches the base ComponentContext with app-level services.
|
|
40
|
+
* Registered once at router creation; called for every context construction.
|
|
41
|
+
*
|
|
42
|
+
* **1. Register** — always spread `base` to preserve routing/file/signal data:
|
|
43
|
+
* ```ts
|
|
44
|
+
* createSpaHtmlRouter(manifest, {
|
|
45
|
+
* extendContext: (base) => ({ ...base, rpc: myRpcClient }),
|
|
46
|
+
* });
|
|
47
|
+
* ```
|
|
48
|
+
*
|
|
49
|
+
* **2. Access** — expose custom properties to components via module augmentation:
|
|
50
|
+
* ```ts
|
|
51
|
+
* declare module '@emkodev/emroute' {
|
|
52
|
+
* interface ComponentContext { rpc: RpcClient; }
|
|
53
|
+
* }
|
|
54
|
+
* ```
|
|
55
|
+
* or per-component via the third generic:
|
|
56
|
+
* ```ts
|
|
57
|
+
* class MyPage extends PageComponent<Params, Data, AppContext> {}
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export type ContextProvider = (base: ComponentContext) => ComponentContext;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render context determines how components are rendered.
|
|
64
|
+
*/
|
|
65
|
+
export type RenderContext = 'markdown' | 'html' | 'spa';
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Abstract base class for all components.
|
|
69
|
+
*
|
|
70
|
+
* Subclasses must implement:
|
|
71
|
+
* - name: unique identifier for custom element tag
|
|
72
|
+
* - getData(): fetch/compute data
|
|
73
|
+
* - renderMarkdown(): render as markdown
|
|
74
|
+
*
|
|
75
|
+
* Optional override:
|
|
76
|
+
* - renderHTML(): custom HTML rendering (defaults to markdown→HTML conversion)
|
|
77
|
+
* - validateParams(): params validation
|
|
78
|
+
*
|
|
79
|
+
* @typeParam TContext — custom context shape; defaults to ComponentContext.
|
|
80
|
+
* Use with `extendContext` on the router to inject app-level services.
|
|
81
|
+
* See {@link ContextProvider} for details.
|
|
82
|
+
*/
|
|
83
|
+
export abstract class Component<
|
|
84
|
+
TParams = unknown,
|
|
85
|
+
TData = unknown,
|
|
86
|
+
TContext extends ComponentContext = ComponentContext,
|
|
87
|
+
> {
|
|
88
|
+
/** Type carrier for getData args — use as `this['DataArgs']` in overrides. */
|
|
89
|
+
declare readonly DataArgs: {
|
|
90
|
+
params: TParams;
|
|
91
|
+
signal?: AbortSignal;
|
|
92
|
+
context: TContext;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/** Type carrier for render args — use as `this['RenderArgs']` in overrides. */
|
|
96
|
+
declare readonly RenderArgs: {
|
|
97
|
+
data: TData | null;
|
|
98
|
+
params: TParams;
|
|
99
|
+
context: TContext;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/** Unique name in kebab-case. Used for custom element: `<widget-{name}>` */
|
|
103
|
+
abstract readonly name: string;
|
|
104
|
+
|
|
105
|
+
/** Host element reference, set by ComponentElement in the browser. */
|
|
106
|
+
element?: HTMLElement;
|
|
107
|
+
|
|
108
|
+
/** Associated file paths for pre-loaded content (html, md, css). */
|
|
109
|
+
readonly files?: { html?: string; md?: string; css?: string };
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* When true, SSR serializes the getData() result into the element's
|
|
113
|
+
* light DOM so the client can access it immediately in hydrate()
|
|
114
|
+
* without re-fetching.
|
|
115
|
+
*
|
|
116
|
+
* Default is false — hydrate() receives `data: null`. Most widgets
|
|
117
|
+
* don't need this because the rendered Shadow DOM already contains
|
|
118
|
+
* the visual representation of the data.
|
|
119
|
+
*
|
|
120
|
+
* If you find yourself parsing the shadow DOM in hydrate() trying to
|
|
121
|
+
* reconstruct the original data object, set this to true instead.
|
|
122
|
+
* The server-fetched data will be available as `args.data` in hydrate().
|
|
123
|
+
*/
|
|
124
|
+
readonly exposeSsrData?: boolean;
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Fetch or compute data based on params.
|
|
128
|
+
* Called server-side for SSR, client-side for SPA.
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* override async getData({ params, signal }: this['DataArgs']) {
|
|
133
|
+
* const res = await fetch(`/api/${params.id}`, { signal });
|
|
134
|
+
* return res.json();
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*/
|
|
138
|
+
abstract getData(args: this['DataArgs']): Promise<TData | null>;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Render as markdown.
|
|
142
|
+
* This is the canonical content representation.
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* override renderMarkdown({ data }: this['RenderArgs']) {
|
|
147
|
+
* return `# ${data?.title}`;
|
|
148
|
+
* }
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
abstract renderMarkdown(args: this['RenderArgs']): string;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Render as HTML for browser context.
|
|
155
|
+
*
|
|
156
|
+
* Default implementation converts renderMarkdown() output to HTML.
|
|
157
|
+
* Override for custom HTML rendering with rich styling/interactivity.
|
|
158
|
+
*/
|
|
159
|
+
renderHTML(args: this['RenderArgs']): string {
|
|
160
|
+
if (args.data === null) {
|
|
161
|
+
return `<div data-component="${this.name}">Loading...</div>`;
|
|
162
|
+
}
|
|
163
|
+
// Default: wrap markdown in a container
|
|
164
|
+
// The actual markdown→HTML conversion happens at render time
|
|
165
|
+
const markdown = this.renderMarkdown({
|
|
166
|
+
data: args.data,
|
|
167
|
+
params: args.params,
|
|
168
|
+
context: args.context,
|
|
169
|
+
});
|
|
170
|
+
return `<div data-component="${this.name}" data-markdown>${escapeHtml(markdown)}</div>`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hydration hook called after SSR content is adopted or after SPA rendering.
|
|
175
|
+
* Use to attach event listeners to existing DOM without re-rendering.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* override hydrate({ data, params, context }: this['RenderArgs']) {
|
|
180
|
+
* const button = this.element?.querySelector('button');
|
|
181
|
+
* button?.addEventListener('click', () => this.deleteItem(data.id));
|
|
182
|
+
* }
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
hydrate?(args: this['RenderArgs']): void;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Cleanup hook called when the component is removed from the DOM.
|
|
189
|
+
* Use for clearing timers, removing event listeners, unmounting
|
|
190
|
+
* third-party renderers, closing connections, etc.
|
|
191
|
+
*
|
|
192
|
+
* Intentionally synchronous (called from disconnectedCallback). You can
|
|
193
|
+
* fire async cleanup here, but it will not be awaited.
|
|
194
|
+
*/
|
|
195
|
+
destroy?(): void;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Validate params.
|
|
199
|
+
* @returns Error message if invalid, undefined if valid.
|
|
200
|
+
*/
|
|
201
|
+
validateParams?(params: TParams): string | undefined;
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Render error state.
|
|
205
|
+
*/
|
|
206
|
+
renderError(args: { error: unknown; params: TParams }): string {
|
|
207
|
+
const msg = args.error instanceof Error ? args.error.message : String(args.error);
|
|
208
|
+
return `<div data-component="${this.name}">Error: ${escapeHtml(msg)}</div>`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Render error as markdown.
|
|
213
|
+
*/
|
|
214
|
+
renderMarkdownError(error: unknown): string {
|
|
215
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
216
|
+
return `> **Error** (\`${this.name}\`): ${msg}`;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Component manifest entry for code generation.
|
|
222
|
+
*/
|
|
223
|
+
export interface ComponentManifestEntry {
|
|
224
|
+
name: string;
|
|
225
|
+
modulePath: string;
|
|
226
|
+
tagName: string;
|
|
227
|
+
type: 'page' | 'widget';
|
|
228
|
+
pattern?: string;
|
|
229
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Page Component
|
|
3
|
+
*
|
|
4
|
+
* Page component — params come from URL, context carries file content.
|
|
5
|
+
*
|
|
6
|
+
* Default implementations follow the fallback table:
|
|
7
|
+
* - renderHTML: html file → md via <mark-down> → <router-slot /> (non-leaf only)
|
|
8
|
+
* - renderMarkdown: md file → ```router-slot\n``` (non-leaf only)
|
|
9
|
+
* - getData: no-op (returns null)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Component, type ComponentContext } from './abstract.component.ts';
|
|
13
|
+
import { escapeHtml } from '../util/html.util.ts';
|
|
14
|
+
|
|
15
|
+
export class PageComponent<
|
|
16
|
+
TParams extends Record<string, string> = Record<string, string>,
|
|
17
|
+
TData = unknown,
|
|
18
|
+
TContext extends ComponentContext = ComponentContext,
|
|
19
|
+
> extends Component<TParams, TData, TContext> {
|
|
20
|
+
override readonly name: string = 'page';
|
|
21
|
+
|
|
22
|
+
/** Route pattern this page handles (optional — set by subclasses) */
|
|
23
|
+
readonly pattern?: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch or compute page data. Override in subclasses.
|
|
27
|
+
* Default: returns null (no data needed).
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* ```ts
|
|
31
|
+
* override getData({ params, context }: this['DataArgs']) {
|
|
32
|
+
* return fetch(`/api/${params.id}`, { signal: context?.signal });
|
|
33
|
+
* }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
override getData(
|
|
37
|
+
_args: this['DataArgs'],
|
|
38
|
+
): Promise<TData | null> {
|
|
39
|
+
return Promise.resolve(null);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Render page as HTML.
|
|
44
|
+
*
|
|
45
|
+
* Fallback chain:
|
|
46
|
+
* 1. html file content from context
|
|
47
|
+
* 2. md file content wrapped in `<mark-down>`
|
|
48
|
+
* 3. `<router-slot />` (bare slot for child routes)
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* override renderHTML({ data, params, context }: this['RenderArgs']) {
|
|
53
|
+
* return `<h1>${params.id}</h1><p>${context?.files?.html ?? ''}</p>`;
|
|
54
|
+
* }
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
override renderHTML(
|
|
58
|
+
args: this['RenderArgs'],
|
|
59
|
+
): string {
|
|
60
|
+
const files = args.context.files;
|
|
61
|
+
const style = files?.css ? `<style>${files.css}</style>\n` : '';
|
|
62
|
+
|
|
63
|
+
if (files?.html) {
|
|
64
|
+
let html = style + files.html;
|
|
65
|
+
if (files.md && html.includes('<mark-down></mark-down>')) {
|
|
66
|
+
html = html.replace(
|
|
67
|
+
'<mark-down></mark-down>',
|
|
68
|
+
`<mark-down>${escapeHtml(files.md)}</mark-down>`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return html;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (files?.md) {
|
|
75
|
+
// HOTFIX: skip external <router-slot> when markdown already defines one
|
|
76
|
+
// via ```router-slot fenced block. Without this, SPA mode produces two
|
|
77
|
+
// slots with the same pattern — one inside <mark-down> (from the fenced
|
|
78
|
+
// block) and one outside (from this suffix). SSR strips empty duplicates
|
|
79
|
+
// via stripSlots, but SPA has no equivalent cleanup.
|
|
80
|
+
// See: issues/pending/spa-duplicate-router-slot.issue.md
|
|
81
|
+
const hasSlot = files.md.includes('```router-slot');
|
|
82
|
+
const slot = args.context.isLeaf || hasSlot ? '' : '\n<router-slot></router-slot>';
|
|
83
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>${slot}`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return args.context.isLeaf ? '' : '<router-slot></router-slot>';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render page as Markdown.
|
|
91
|
+
*
|
|
92
|
+
* Fallback chain:
|
|
93
|
+
* 1. md file content from context
|
|
94
|
+
* 2. `` ```router-slot\n``` `` (slot placeholder in markdown — newline required)
|
|
95
|
+
*
|
|
96
|
+
* @example
|
|
97
|
+
* ```ts
|
|
98
|
+
* override renderMarkdown({ data, params, context }: this['RenderArgs']) {
|
|
99
|
+
* return `# ${params.id}\n\n${context?.files?.md ?? ''}`;
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
override renderMarkdown(
|
|
104
|
+
args: this['RenderArgs'],
|
|
105
|
+
): string {
|
|
106
|
+
const files = args.context.files;
|
|
107
|
+
|
|
108
|
+
if (files?.md) {
|
|
109
|
+
return files.md;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return args.context.isLeaf ? '' : '```router-slot\n```';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Page title. Override in subclasses.
|
|
117
|
+
* Default: undefined (no title).
|
|
118
|
+
*
|
|
119
|
+
* @example
|
|
120
|
+
* ```ts
|
|
121
|
+
* override getTitle({ data, params }: this['RenderArgs']) {
|
|
122
|
+
* return `Project ${params.id}`;
|
|
123
|
+
* }
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
getTitle(
|
|
127
|
+
_args: this['RenderArgs'],
|
|
128
|
+
): string | undefined {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Shared default instance used by renderers when no custom .page.ts exists. */
|
|
134
|
+
export default new PageComponent();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WidgetComponent — embeddable unit within page content.
|
|
3
|
+
*
|
|
4
|
+
* Everything reusable that is not a page is a Widget.
|
|
5
|
+
* Widgets render across all contexts (HTML, Markdown, SPA) and are
|
|
6
|
+
* resolved by name via WidgetRegistry.
|
|
7
|
+
*
|
|
8
|
+
* Pages live in the routes manifest. Widgets live in the registry.
|
|
9
|
+
*
|
|
10
|
+
* Default rendering fallback chains (parallel to PageComponent):
|
|
11
|
+
* - renderHTML: html file → md file in <mark-down> → base Component default
|
|
12
|
+
* - renderMarkdown: md file → ''
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { Component, type ComponentContext } from './abstract.component.ts';
|
|
16
|
+
import { escapeHtml, scopeWidgetCss } from '../util/html.util.ts';
|
|
17
|
+
|
|
18
|
+
export abstract class WidgetComponent<
|
|
19
|
+
TParams = unknown,
|
|
20
|
+
TData = unknown,
|
|
21
|
+
TContext extends ComponentContext = ComponentContext,
|
|
22
|
+
> extends Component<TParams, TData, TContext> {
|
|
23
|
+
/**
|
|
24
|
+
* Render widget as HTML.
|
|
25
|
+
*
|
|
26
|
+
* Fallback chain:
|
|
27
|
+
* 1. html file content from context
|
|
28
|
+
* 2. md file content wrapped in `<mark-down>`
|
|
29
|
+
* 3. base Component default (markdown→HTML conversion)
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* override renderHTML({ data, params }: this['RenderArgs']) {
|
|
34
|
+
* return `<span>${params.coin}: $${data?.price}</span>`;
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
override renderHTML(
|
|
39
|
+
args: this['RenderArgs'],
|
|
40
|
+
): string {
|
|
41
|
+
const files = args.context.files;
|
|
42
|
+
// @scope needed for SSR Light DOM output; redundant but harmless in SPA Shadow DOM
|
|
43
|
+
const style = files?.css ? `<style>${scopeWidgetCss(files.css, this.name)}</style>\n` : '';
|
|
44
|
+
|
|
45
|
+
if (files?.html) {
|
|
46
|
+
return style + files.html;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (files?.md) {
|
|
50
|
+
return `${style}<mark-down>${escapeHtml(files.md)}</mark-down>`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (style) {
|
|
54
|
+
return style + super.renderHTML(args);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return super.renderHTML(args);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Render widget as Markdown.
|
|
62
|
+
*
|
|
63
|
+
* Fallback chain:
|
|
64
|
+
* 1. md file content from context
|
|
65
|
+
* 2. empty string
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* override renderMarkdown({ data, params }: this['RenderArgs']) {
|
|
70
|
+
* return `**${params.coin}**: $${data?.price}`;
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
override renderMarkdown(
|
|
75
|
+
args: this['RenderArgs'],
|
|
76
|
+
): string {
|
|
77
|
+
const files = args.context.files;
|
|
78
|
+
|
|
79
|
+
if (files?.md) {
|
|
80
|
+
return files.md;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return '';
|
|
84
|
+
}
|
|
85
|
+
}
|