@xyd-js/composer 0.0.0-build

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.
@@ -0,0 +1,606 @@
1
+ import * as React from "react"
2
+ import type { RootContent } from "mdast";
3
+ import { toMarkdown } from "mdast-util-to-markdown";
4
+ import { mdxToMarkdown } from "mdast-util-mdx";
5
+ import { toHast } from 'mdast-util-to-hast'
6
+ import { toHtml } from 'hast-util-to-html'
7
+ import { parse } from '@babel/parser';
8
+ import { highlight } from "codehike/code";
9
+ import { marked } from 'marked';
10
+ import { fromMarkdown } from 'mdast-util-from-markdown';
11
+ import { htmlToJsx } from "html-to-jsx-transform";
12
+
13
+ import { type AtlasProps } from "@xyd-js/atlas";
14
+ import { Metadata, type Theme as ThemeSettings, Settings } from "@xyd-js/core"
15
+
16
+ import { VarCode, ContentFS } from "@xyd-js/content";
17
+ import { markdownPlugins } from "@xyd-js/content/md";
18
+ import { ExampleRoot, Definition, DefinitionProperty } from "@xyd-js/uniform";
19
+
20
+ import { metaComponent } from './decorators';
21
+
22
+ // TODO: !!!! REFACTOR !!!
23
+
24
+ interface AtlasVars {
25
+ examples?: VarCode
26
+ }
27
+
28
+ // TODO: get object from atlas
29
+ // Define the type for an example object
30
+ interface ExampleObject {
31
+ codeblock: {
32
+ title: string;
33
+ tabs: {
34
+ title: string;
35
+ language: string;
36
+ code: string;
37
+ highlighted: any; // TODO: fix
38
+ }[];
39
+ };
40
+ }
41
+
42
+ // Define the type for an example group
43
+ interface ExampleGroup {
44
+ examples: ExampleObject[];
45
+ }
46
+
47
+
48
+
49
+ export class Composer {
50
+ @metaComponent("home", "PageHome")
51
+ private async homeMetaComponent(
52
+ themeSettings: ThemeSettings,
53
+ props: any,
54
+ vars: AtlasVars,
55
+ treeChilds: readonly RootContent[],
56
+ meta: Metadata
57
+ ) {
58
+ if (meta) {
59
+ meta.layout = "page"
60
+ }
61
+ return props
62
+ }
63
+
64
+ @metaComponent("bloghome", "PageBlogHome")
65
+ private async blogHomeMetaComponent(
66
+ themeSettings: ThemeSettings,
67
+ props: any,
68
+ vars: AtlasVars,
69
+ treeChilds: readonly RootContent[],
70
+ meta: Metadata
71
+ ) {
72
+ if (meta) {
73
+ meta.layout = "page"
74
+ }
75
+ return props
76
+ }
77
+
78
+
79
+ @metaComponent("firstslide", "PageFirstSlide")
80
+ private async firstSlideMetaComponent(
81
+ themeSettings: ThemeSettings,
82
+ props: any,
83
+ vars: AtlasVars,
84
+ treeChilds: readonly RootContent[],
85
+ meta: Metadata,
86
+ settings: Settings
87
+ ) {
88
+ if (meta) {
89
+ meta.layout = "page"
90
+ }
91
+
92
+ if (props.rightContent) {
93
+ // Use existing markdown processing pipeline
94
+ const mdPlugins = await markdownPlugins({
95
+ maxDepth: 2,
96
+ }, settings)
97
+
98
+ const contentFs = new ContentFS(settings, mdPlugins.remarkPlugins, mdPlugins.rehypePlugins, mdPlugins.recmaPlugins)
99
+
100
+ try {
101
+ const compiledContent = await contentFs.compileContent(props.rightContent)
102
+ const mdxResult = mdxExport(compiledContent)
103
+
104
+ if (mdxResult && mdxResult.default) {
105
+ const MDXComponent = mdxResult.default
106
+ // TODO: !!! support all components from react content !!!
107
+ const reactElement = MDXComponent({
108
+ components: {
109
+ "DirectiveCodeGroup": "DirectiveCodeGroup",
110
+ "Callout": "Callout",
111
+ }
112
+ })
113
+ props.rightContent = reactElement
114
+ }
115
+
116
+ } catch (error) {
117
+ console.error('Error processing rightContent:', error)
118
+ // Fallback to original content if processing fails
119
+ }
120
+ }
121
+
122
+ return props
123
+ }
124
+
125
+ // TODO: !!! COMPOSE API !!!!
126
+ // TODO: this.themeSettings but currently issues with decorators
127
+ @metaComponent("atlas", "Atlas")
128
+ private async atlasMetaComponent(
129
+ themeSettings: ThemeSettings,
130
+ props: AtlasProps,
131
+ vars: AtlasVars,
132
+ treeChilds: readonly RootContent[]
133
+ ) {
134
+ if (!props.references) {
135
+ props.references = []
136
+ }
137
+
138
+ //@ts-ignore
139
+ const outputVarExamples: ExampleRoot = {
140
+ groups: []
141
+ }
142
+
143
+ const oneExample = vars.examples?.length === 1 && !Array.isArray(vars.examples[0])
144
+
145
+ // Helper function to create an example object
146
+ function createExampleObject(example: any): ExampleObject {
147
+ // Extract the highlighted property correctly
148
+ const highlighted = example.highlighted || example;
149
+
150
+ return {
151
+ codeblock: {
152
+ title: example.meta,
153
+ tabs: [
154
+ {
155
+ title: example.meta,
156
+ language: example.lang,
157
+ code: example.code,
158
+ highlighted: highlighted
159
+ }
160
+ ]
161
+ }
162
+ };
163
+ }
164
+
165
+ if (oneExample) {
166
+ const example = vars.examples?.[0]
167
+ if (Array.isArray(example) || !example) {
168
+ return
169
+ }
170
+
171
+ outputVarExamples.groups.push({
172
+ examples: [createExampleObject(example)]
173
+ })
174
+ } else {
175
+ // Process each example or group of examples
176
+ vars.examples?.forEach((item) => {
177
+ if (Array.isArray(item)) {
178
+ // This is a group with a title as the first element
179
+ const groupTitle = item[0];
180
+ const groupExamples = item.slice(1);
181
+
182
+ const exampleGroup: ExampleGroup = {
183
+ examples: []
184
+ };
185
+
186
+ // Process each example in the group
187
+ groupExamples.forEach((example) => {
188
+ if (example && typeof example === 'object') {
189
+ exampleGroup.examples.push(createExampleObject(example));
190
+ }
191
+ });
192
+
193
+ if (exampleGroup.examples.length > 0) {
194
+ outputVarExamples.groups.push(exampleGroup);
195
+ }
196
+ } else if (item && typeof item === 'object') {
197
+ // This is a single example
198
+ const exampleGroup: ExampleGroup = {
199
+ examples: [createExampleObject(item)]
200
+ };
201
+
202
+ outputVarExamples.groups.push(exampleGroup);
203
+ }
204
+ });
205
+ }
206
+
207
+ const reactElements: React.ReactNode[] = []
208
+
209
+ // TODO: !! IMPORTANT !! BETTER COMPOSE TRANSFORMATION
210
+ for (const child of treeChilds) {
211
+ if (isStandardMdastType(child.type)) {
212
+ const hast = toHast(child)
213
+ const html = toHtml(hast)
214
+
215
+ if (child.type === "heading" && child.depth === 1) {
216
+ const firstChild = child.children[0];
217
+ if (firstChild.type === 'text' && props.references[0]) {
218
+ props.references[0].title = firstChild.value;
219
+ }
220
+ } else {
221
+ const jsx = htmlToJsx(html);
222
+ const reactTree = jsxStringToReactTree(jsx);
223
+ if (reactTree) {
224
+ reactElements.push(reactTree)
225
+ }
226
+ }
227
+
228
+ continue
229
+ } else if (isMdxElement(child.type)) {
230
+ const jsxString = toMarkdown(child, {
231
+ extensions: [mdxToMarkdown()],
232
+ handlers: {// TODO: find better solution how to convert such as structure?
233
+ list(node, parent, context) {
234
+ // call containerFlow as a method on context, so `this` stays correct
235
+ const items = node.children
236
+ .map(item => context.containerFlow(item, node))
237
+ .join('\n')
238
+ return `<ul>\n${items}\n</ul>`
239
+ },
240
+ listItem(node, parent, context) {
241
+ // Use a type assertion to handle the parent parameter
242
+ const content = context.containerFlow(node, parent as any).trim()
243
+ return `<li>${content}</li>`
244
+ },
245
+ }
246
+ });
247
+
248
+ const reactTree = jsxStringToReactTree(jsxString)
249
+ if (reactTree) {
250
+ reactElements.push(reactTree)
251
+ }
252
+ }
253
+ }
254
+
255
+ const propDescription = props.references?.[0]?.description
256
+ if (propDescription) {
257
+ // Sanitize frontmatter description
258
+ if (typeof propDescription === "string") {
259
+ // Remove frontmatter using regex
260
+ const content = propDescription.replace(/^---[\s\S]*?---\n/, '');
261
+
262
+ const md = fromMarkdown(content)
263
+ const hast = toHast(md)
264
+ const html = toHtml(hast)
265
+ const jsx = htmlToJsx(html);
266
+ const reactTree = jsxStringToReactTree(jsx);
267
+ if (reactTree) {
268
+ props.references[0].description = reactTree;
269
+ } else {
270
+ props.references[0].description = content
271
+ }
272
+ }
273
+ }
274
+
275
+ if (reactElements.length > 0) {
276
+ if (props.references?.[0]?.description) {
277
+ reactElements.unshift(props.references[0].description)
278
+ }
279
+ // Create a combined React element from all the elements
280
+ const combinedReactTree = React.createElement(React.Fragment, null, ...reactElements)
281
+
282
+ if (props.references?.[0]) {
283
+ props.references[0].description = combinedReactTree
284
+ }
285
+ }
286
+
287
+ if (
288
+ !outputVarExamples.groups.length &&
289
+ props.references[0]?.examples.groups?.length
290
+ ) {
291
+ const promises: Promise<void>[] = []
292
+
293
+ props.references[0].examples?.groups.forEach(group => {
294
+ group.examples.forEach(example => {
295
+ example.codeblock.tabs.forEach(tab => {
296
+
297
+ async function highlightCode() {
298
+ const highlighted = await highlight({
299
+ value: tab.code,
300
+ lang: tab.language,
301
+ meta: tab.title,
302
+ }, themeSettings?.coder?.syntaxHighlight || "github-dark")
303
+
304
+ tab.highlighted = highlighted
305
+ }
306
+
307
+ promises.push(highlightCode());
308
+ });
309
+ });
310
+ });
311
+
312
+ await Promise.all(promises);
313
+ } else {
314
+ if (props.references?.[0]) {
315
+ props.references[0].examples = outputVarExamples;
316
+ }
317
+ }
318
+
319
+ // Process definition properties recursively to convert markdown descriptions to React trees
320
+ if (props.references?.[0]?.definitions) {
321
+ props.references[0].definitions = processDefinitionProperties(props.references[0].definitions);
322
+ }
323
+
324
+ return props
325
+ // TODO: in the future return a component directly here but we need good mechanism for transpiling?
326
+ }
327
+ }
328
+
329
+ function buildElement(node) {
330
+ if (!node) return null;
331
+ switch (node.type) {
332
+ case 'JSXElement': {
333
+ // Resolve type (string for custom components)
334
+ let type;
335
+ const nameNode = node.openingElement.name;
336
+ if (nameNode.type === 'JSXMemberExpression') {
337
+ // flatten Foo.Bar to "Foo.Bar"
338
+ const parts: string[] = [];
339
+ let curr = nameNode;
340
+ while (curr) {
341
+ if (curr.property) parts.unshift(curr.property.name);
342
+ curr = curr.object;
343
+ }
344
+ type = parts.join('.');
345
+ } else {
346
+ type = nameNode.name;
347
+ }
348
+
349
+ // Props
350
+ const props = {};
351
+ for (const attr of node.openingElement.attributes) {
352
+ if (attr.type === 'JSXSpreadAttribute') {
353
+ Object.assign(props, evaluateExpression(attr.argument));
354
+ continue;
355
+ }
356
+ const key = attr.name.name;
357
+ let value;
358
+ if (!attr.value) {
359
+ value = true;
360
+ } else if (attr.value.type === 'StringLiteral' || attr.value.type === 'NumericLiteral' || attr.value.type === 'BooleanLiteral') {
361
+ value = attr.value.value;
362
+ } else if (attr.value.type === 'JSXExpressionContainer') {
363
+ value = evaluateExpression(attr.value.expression);
364
+ }
365
+ props[key] = value;
366
+ }
367
+
368
+ // Children
369
+ const children = node.children
370
+ .map(child => {
371
+ if (child.type === 'JSXText') {
372
+ const text = child.value.replace(/\s+/g, ' ');
373
+ return text.trim() ? text : null;
374
+ }
375
+ if (child.type === 'JSXExpressionContainer') {
376
+ return child.expression.type === 'JSXElement' || child.expression.type === 'JSXFragment'
377
+ ? buildElement(child.expression)
378
+ : evaluateExpression(child.expression);
379
+ }
380
+ if (child.type === 'JSXElement' || child.type === 'JSXFragment') {
381
+ return buildElement(child);
382
+ }
383
+ return null;
384
+ })
385
+ .filter(c => c !== null);
386
+
387
+ // Create React element
388
+ return React.createElement(type, props, ...children);
389
+ }
390
+
391
+ case 'JSXFragment': {
392
+ const children = node.children
393
+ .map(child => (child.type === 'JSXElement' || child.type === 'JSXFragment')
394
+ ? buildElement(child)
395
+ : (child.type === 'JSXText' ? child.value.trim() || null : null)
396
+ )
397
+ .filter(Boolean);
398
+ return React.createElement(React.Fragment, null, ...children);
399
+ }
400
+
401
+ default:
402
+ return null;
403
+ }
404
+ }
405
+
406
+ // Simplistic evaluator for static expressions: identifiers -> undefined, literals -> value, objects -> {}
407
+ function evaluateExpression(expr) {
408
+ switch (expr.type) {
409
+ case 'StringLiteral': return expr.value;
410
+ case 'NumericLiteral': return expr.value;
411
+ case 'BooleanLiteral': return expr.value;
412
+ case 'ObjectExpression': {
413
+ const obj = {};
414
+ for (const prop of expr.properties) {
415
+ if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
416
+ obj[prop.key.name] = evaluateExpression(prop.value);
417
+ }
418
+ }
419
+ return obj;
420
+ }
421
+ // Add more cases (Identifier, CallExpression, etc.) as needed
422
+ default:
423
+ return undefined;
424
+ }
425
+ }
426
+
427
+ // 4) Locate first JSX node
428
+ function findJSX(node) {
429
+ if (!node || typeof node !== 'object') return null;
430
+ if (node.type === 'JSXElement' || node.type === 'JSXFragment') return node;
431
+ for (const key of Object.keys(node)) {
432
+ const val = node[key];
433
+ if (Array.isArray(val)) {
434
+ for (const child of val) {
435
+ const found = findJSX(child);
436
+ if (found) return found;
437
+ }
438
+ } else {
439
+ const found = findJSX(val);
440
+ if (found) return found;
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+
446
+ function jsxStringToReactTree(jsxString: string = "") {
447
+ const ast = parse(jsxString, { sourceType: 'module', plugins: ['jsx'] });
448
+ const rootJSX = findJSX(ast);
449
+ if (!rootJSX) {
450
+ return null
451
+ }
452
+
453
+ const reactTree = buildElement(rootJSX)
454
+
455
+ return reactTree
456
+ }
457
+
458
+ /**
459
+ * Recursively processes definition properties to convert markdown descriptions to React trees
460
+ * @param definitions The definitions to process
461
+ * @returns The processed definitions with markdown descriptions converted to React trees
462
+ */
463
+ function processDefinitionProperties(definitions: Definition[]): Definition[] {
464
+ if (!definitions || !Array.isArray(definitions)) {
465
+ return definitions;
466
+ }
467
+
468
+ return definitions.map(definition => {
469
+ // Process variants recursively
470
+ if (definition.variants && Array.isArray(definition.variants)) {
471
+ for (const variant of definition.variants) {
472
+ if (variant.properties && Array.isArray(variant.properties)) {
473
+ variant.properties = processDefinitionProperty(variant.properties);
474
+ }
475
+ }
476
+ }
477
+
478
+ // Process the definition's properties recursively
479
+ if (definition.properties && Array.isArray(definition.properties)) {
480
+ definition.properties = processDefinitionProperty(definition.properties);
481
+ }
482
+
483
+ if (definition.description && typeof definition.description === "string") {
484
+ const md = fromMarkdown(definition.description)
485
+ const hast = toHast(md)
486
+ const html = toHtml(hast)
487
+ const jsx = htmlToJsx(html);
488
+ const reactTree = jsxStringToReactTree(jsx);
489
+ if (reactTree) {
490
+ definition.description = reactTree;
491
+ }
492
+ }
493
+ return definition;
494
+ });
495
+ }
496
+
497
+ /**
498
+ * Recursively processes definition properties to convert markdown descriptions to React trees
499
+ * @param properties The properties to process
500
+ * @returns The processed properties with markdown descriptions converted to React trees
501
+ */
502
+ function processDefinitionProperty(properties: DefinitionProperty[]): DefinitionProperty[] {
503
+ if (!properties || !Array.isArray(properties)) {
504
+ return properties;
505
+ }
506
+
507
+ return properties.map(property => {
508
+ const newProperty: DefinitionProperty = {
509
+ ...property,
510
+ };
511
+
512
+ if (typeof newProperty.description === 'string' && isMarkdownText(newProperty.description)) {
513
+ const mdast = fromMarkdown(newProperty.description);
514
+ const hast = toHast(mdast);
515
+ const html = toHtml(hast);
516
+ const jsx = htmlToJsx(html);
517
+ const reactTree = jsxStringToReactTree(jsx);
518
+ if (reactTree) {
519
+ newProperty.description = reactTree;
520
+ }
521
+ }
522
+
523
+ if (property.properties && Array.isArray(property.properties)) {
524
+ newProperty.properties = processDefinitionProperty(property.properties);
525
+ }
526
+
527
+ return newProperty;
528
+ });
529
+ }
530
+
531
+ /**
532
+ * Returns true if the input contains any non-plain-text Markdown tokens.
533
+ */
534
+ function isMarkdownText(text: string) {
535
+ // 1) Lex into a token tree
536
+ const tokens = marked.lexer(text);
537
+ let found = false;
538
+
539
+ // 2) Traverse *every* token (including nested) and flag any non-plain-text kinds
540
+ marked.walkTokens(tokens, token => {
541
+ // ignore pure text/whitespace
542
+ if (!['text', 'paragraph', 'space', 'newline'].includes(token.type)) {
543
+ found = true;
544
+ }
545
+ });
546
+
547
+ return found;
548
+ }
549
+
550
+ /**
551
+ * List of standard MDAST node types
552
+ */
553
+ const standardMdastTypes = [
554
+ 'root',
555
+ 'paragraph',
556
+ 'heading',
557
+ 'text',
558
+ 'emphasis',
559
+ 'strong',
560
+ 'delete',
561
+ 'blockquote',
562
+ 'code',
563
+ 'link',
564
+ 'image',
565
+ 'list',
566
+ 'listItem',
567
+ 'table',
568
+ 'tableRow',
569
+ 'tableCell',
570
+ 'html',
571
+ 'break',
572
+ 'thematicBreak',
573
+ 'definition',
574
+ 'footnoteDefinition',
575
+ 'footnoteReference',
576
+ 'inlineCode',
577
+ 'linkReference',
578
+ 'imageReference',
579
+ 'footnote',
580
+ 'tableCaption'
581
+ ];
582
+
583
+
584
+ /**
585
+ * Checks if a given type is a standard MDAST type
586
+ * @param type The node type to check
587
+ * @returns True if the type is a standard MDAST type, false otherwise
588
+ */
589
+ function isStandardMdastType(type: string): boolean {
590
+ return standardMdastTypes.includes(type);
591
+ }
592
+
593
+ function isMdxElement(type: string): boolean {
594
+ return type === 'mdxJsxFlowElement'
595
+ }
596
+
597
+ function mdxExport(code: string) {
598
+ const scope = {
599
+ Fragment: React.Fragment,
600
+ jsxs: React.createElement,
601
+ jsx: React.createElement,
602
+ jsxDEV: React.createElement,
603
+ }
604
+ const fn = new Function(...Object.keys(scope), code)
605
+ return fn(scope)
606
+ }
@@ -0,0 +1,18 @@
1
+ import { registerMetaComponent } from "@xyd-js/context";
2
+
3
+ export function metaComponent<P, V>(
4
+ name: string,
5
+ componentName?: string
6
+ ) {
7
+ return function (
8
+ target: any,
9
+ context: any
10
+ ) {
11
+ registerMetaComponent(
12
+ name,
13
+ componentName || name,
14
+ target
15
+ );
16
+ };
17
+ }
18
+
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export {
2
+ Composer
3
+ } from "./Composer"
package/tsconfig.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "compilerOptions": {
3
+ "paths": {
4
+ "@xyd-js/framework/react": [
5
+ "../xyd-framework/packages/react/index.ts"
6
+ ],
7
+ "@xyd-js/context": [
8
+ "../xyd-context/src/index.ts"
9
+ ],
10
+ "@xyd-js/core": [
11
+ "../xyd-core/src/index.ts"
12
+ ]
13
+ },
14
+ "module": "esnext",
15
+ "esModuleInterop": true,
16
+ "moduleResolution": "bundler",
17
+ "target": "ES6",
18
+ "lib": [
19
+ "dom",
20
+ "dom.iterable",
21
+ "esnext"
22
+ ],
23
+ "allowJs": true,
24
+ "skipLibCheck": true,
25
+ "strict": false,
26
+ "noEmit": true,
27
+ "incremental": false,
28
+ "resolveJsonModule": true,
29
+ "isolatedModules": true,
30
+ "jsx": "preserve",
31
+ "plugins": [
32
+ {
33
+ "name": "next"
34
+ }
35
+ ],
36
+ "strictNullChecks": true,
37
+ "typeRoots": [
38
+ "./types.d.ts"
39
+ ]
40
+ },
41
+ "include": [
42
+ "src/**/*.ts",
43
+ "src/**/*.tsx",
44
+ "types.d.ts"
45
+ ],
46
+ "exclude": [
47
+ "node_modules"
48
+ ]
49
+ }