File size: 3,374 Bytes
b39afbe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
/**
 * Copyright (c) 2023 MERCENARIES.AI PTE. LTD.
 * All rights reserved.
 */

import Handlebars, { type HelperDelegate } from 'handlebars';
import { marked } from 'marked';

const mdRenderer = new marked.Renderer();
mdRenderer.link = function (_href, _title, _text) {
  const link = marked.Renderer.prototype.link.apply(this, arguments as any);
  return link.replace('<a', "<a target='_blank'");
};

class MarkdownEngine {
  handlebars: typeof Handlebars;
  private asyncResolvers: Record<string, (token: string) => Promise<any>> = {};

  constructor() {
    this.handlebars = Handlebars.create();
  }

  registerAsyncResolver(directive: string, resolverFunction: (token: string) => Promise<any>) {
    this.asyncResolvers[directive] = resolverFunction;
  }

  registerToken(tokenName: string, resolver: HelperDelegate) {
    this.handlebars.registerHelper(tokenName, resolver);
  }

  async getAsyncDataForDirective(directive: string, token: string): Promise<any> {
    const resolver = this.asyncResolvers[directive];
    if (!resolver) {
      throw new Error(`No resolver registered for directive: ${directive}`);
    }
    return await resolver(token);
  }

  extractDirectiveData(statement: any): { name?: string; param?: string } {
    if (statement.type === 'MustacheStatement' || statement.type === 'BlockStatement') {
      const name = statement.path?.original;
      const param = statement.params?.[0]?.original;

      return {
        name,
        param
      };
    }

    return {};
  }

  async preprocessData(markdownContent: string): Promise<{ content: string; data: any }> {
    const data: any = {};

    const tokens = this.extractTokens(markdownContent);

    for (const [placeholder, originalDirective] of tokens) {
      const parsed = Handlebars.parse(originalDirective);
      const directiveData = this.extractDirectiveData(parsed.body[0]);
      const directive = directiveData?.name;
      const token = directiveData?.param;

      if (directive && token) {
        const tokenData = await this.getAsyncDataForDirective(directive, token);
        data[placeholder] = tokenData;
        markdownContent = markdownContent.replace(placeholder, originalDirective);
      }
    }

    return { content: markdownContent, data };
  }

  extractTokens(content: string): Map<string, string> {
    const tokenRegex = /{{(BUTTON|INPUT)[^}]+}}/g;
    const tokens = new Map<string, string>();
    let match;

    while ((match = tokenRegex.exec(content)) !== null) {
      const placeholder = `TOKEN_${tokens.size + 1}`;
      tokens.set(placeholder, match[0]);
      content = content.replace(match[0], placeholder);
    }

    return tokens;
  }

  injectTokens(content: string, tokens: Map<string, string>): string {
    let processedContent = content;

    tokens.forEach((value, key) => {
      processedContent = processedContent.replace(key, value);
    });

    return processedContent;
  }

  async render(markdownContent: string): Promise<string> {
    const tokens = this.extractTokens(markdownContent);
    const md = marked.parse(markdownContent, { renderer: mdRenderer });
    const injectedContent = this.injectTokens(md, tokens);
    const { content, data } = await this.preprocessData(injectedContent);
    const replacedContent = this.handlebars.compile(content)(data);

    return replacedContent;
  }
}

export { MarkdownEngine };