Публікація

Редактор WYSIWYG для работи з Markdown та бібліотеки для обробки Markdown

Збірка бібліотек для роботи з Markdown

Я працюю з редактором TipTap, який побудований на основі іншого редактора Prosemirror і пропонує спрощений API та розширення для роботи з ним. Мої враження від роботи з TipTap суто позитивні. Він зміг вдовольнити всі мої вимоги.

Мої вимоги наступні:

  • Схема контента. Редактор повинен контролювати та очищувати весь контент який потрапляє в нього і приводити його до певної схеми
  • API підтримки обʼєктів-вставок. Редактор повинен підтримувати роботу елементами-обʼєктами, які утримують в середені вставку накшталт відео з Ютуба. Повинен бути простий і зручний API роботи з такими обʼєктами: парсінг таких обʼєктів з контента та збереження вставок у фінальних даних
  • Консистентність при збереженні, завантаженні, вставці контента
  • Підтримка всіх стандартних вставок, накшталт Image
  • Експорт контента як Markdown
  • Зручна кастомізація зовнішньго вигляду
  • Простота конфігурації

По всім цим параментрам TipTap/Prosemirror показав себе відмінно. Щодо консистентності контенту, то це більше заслуга Prosemirror, проте TipTap зробив Prosemirror супер-простим в конфігурації. Я спробував PlateJS, котрий виглядає потужно, проте коли я його конфігурую, в мене складається враження що поріг входу далеко не такий низький, як здається. Плюс слабка документація та підтримка спільнотою робить цей редактор складно конфігуруємим.

Які доробки я приніс в TipTap

В результаті у мене вийшов WYSIWYG редактор, який підтримує кастомні вставки і PHP рендерер, котрий вміє рендерити ці кастомні вставки в потрібний мені HTML.

Розширення TipTap для підтримки кастомних вставок

Кастомна вставка це markdown-конструкція, схожа на конструкцію для вставки зображення, має тип та URL. Потрібна для вставки і виводу віджетів, накшталт Ютуб відео.

1
@[external](https://youtube.com/...)

Конструкція має тип external та посилання на Ютуб. Конструкція вставки відрізняється від конструкції зображення лише знаком @ замість !.

Для роботи розширення потрібно додати бібліотеку posva/markdown-it-custom-block.

Розширення дозволяє обробляти вставку (paste) посилань в редактор та заміняє їх на вставки. Вставки представляє у вигляді веб-компонента <c-external url="..."></c-external>. Розширення може розбирати вхідний контент, шукати конструкції @[...](...) та перетворювати їх на вставки. Розширення підтримує експорт контента в Markdown.

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
111
import { Node, nodePasteRule } from "@tiptap/core";
import customBlock from "markdown-it-custom-block";

export const URL_REGEX_GLOBAL =
    /(https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|www\.[a-zA-Z0-9][a-zA-Z0-9-]+[a-zA-Z0-9]\.[^\s]{2,}|https?:\/\/(?:www\.|(?!www))[a-zA-Z0-9]+\.[^\s]{2,}|www\.[a-zA-Z0-9]+\.[^\s]{2,})/g;

declare module "@tiptap/core" {
    interface Commands<ReturnType> {
        external: {
            setExternal: (options: any) => ReturnType;
        };
    }
}

export interface ExternalOptions {
    url: string;
}

export const External = Node.create<ExternalOptions>({
    name: "external",

    addOptions() {
        return {
            url: "",
        };
    },

    inline() {
        return false;
    },

    group() {
        return "block";
    },

    draggable: true,

    addAttributes() {
        return {
            url: {
                default: null,
            },
        };
    },

    parseHTML() {
        return [
            {
                tag: "c-external",
            },
        ];
    },

    addCommands() {
        return {
            setExternal:
                (options: ExternalOptions) =>
                ({ commands }) => {
                    if (!options.url) {
                        return false;
                    }

                    commands.insertContent({
                        type: this.name,
                        attrs: options,
                    });
                },
        };
    },

    addPasteRules() {
        return [
            nodePasteRule({
                find: URL_REGEX_GLOBAL,
                type: this.type,
                getAttributes: (match) => {
                    return { url: match.input };
                },
            }),
        ];
    },

    addStorage() {
        return {
            markdown: {
                serialize: (state, node) => {
                    state.write(
                        "@[external](" +
                            node.attrs.url.replace(/[\(\)]/g, "\\$&") +
                            ")",
                    );
                    state.write("\n");
                    state.write("\n");
                },
                parse: {
                    setup: (markdownit) => {
                        markdownit.use(customBlock, {
                            external(arg) {
                                return `<c-external url="${arg}"></c-external>`;
                            },
                        });
                    },
                },
            },
        };
    },

    renderHTML({ HTMLAttributes }) {
        return ["c-external", HTMLAttributes];
    },
});

Розширення Commonmark PHP для відображення вставок

Контент я намагаюся рендеити в одному місці. Мав негативний досвід, коли є декілька точок рендерінгу контента і це дуже ускладнює життя. Тому я використовую Commonmark PHP і Laravel хелпер. Проте я додав власне розширення CustomBlockExtension, яке оброблює кастомні вставки, як блоки. Тобто конструкція вставки повинна займати одну строку і не повинна бути інлайновою.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Str::markdown(
    $this->body,
    [
        "html_input" => "strip",
        "allow_unsafe_links" => false,
        "external_link" => [
            "internal_hosts" => [
                parse_url(env("APP_URL"), PHP_URL_HOST),
            ],
            "open_in_new_window" => true,
            "nofollow" => "external",
            "noopener" => "external",
            "noreferrer" => "external",
        ],
    ],
    [new CustomBlockExtension(), new ExternalLinkExtension()]
);

Код CustomBlockExtension знаходиться за посиланням: gist.github.com/jmas/9f6c0fa7275cd59b16358b229f189e82.

Публікація захищена ліцензією CC BY 4.0 .

© jmas. Деякі права захищено.

Powered by Jekyll with Chirpy theme