@gravity-ui/markdown-editor 14.4.0 → 14.5.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.
Files changed (54) hide show
  1. package/build/cjs/bundle/Editor.js +1 -0
  2. package/build/cjs/bundle/config/markup.d.ts +41 -17
  3. package/build/cjs/bundle/config/markup.js +413 -308
  4. package/build/cjs/bundle/config/wysiwyg.d.ts +29 -18
  5. package/build/cjs/bundle/config/wysiwyg.js +526 -310
  6. package/build/cjs/bundle/sticky/sticky.css +1 -1
  7. package/build/cjs/bundle/types.d.ts +2 -0
  8. package/build/cjs/extensions/behavior/Clipboard/utils.d.ts +1 -0
  9. package/build/cjs/extensions/behavior/Clipboard/utils.js +1 -0
  10. package/build/cjs/extensions/markdown/CodeBlock/handle-paste.js +5 -17
  11. package/build/cjs/extensions/yfm/YfmFile/YfmFileSpecs/const.d.ts +12 -0
  12. package/build/cjs/extensions/yfm/YfmFile/YfmFileSpecs/const.js +21 -2
  13. package/build/cjs/extensions/yfm/YfmFile/YfmFileSpecs/index.d.ts +8 -1
  14. package/build/cjs/extensions/yfm/YfmFile/YfmFileSpecs/index.js +29 -5
  15. package/build/cjs/markup/codemirror/create.d.ts +1 -0
  16. package/build/cjs/markup/codemirror/create.js +41 -4
  17. package/build/cjs/markup/codemirror/html-to-markdown/converters.d.ts +111 -0
  18. package/build/cjs/markup/codemirror/html-to-markdown/converters.js +214 -0
  19. package/build/cjs/markup/codemirror/html-to-markdown/handlers.d.ts +104 -0
  20. package/build/cjs/markup/codemirror/html-to-markdown/handlers.js +233 -0
  21. package/build/cjs/markup/codemirror/html-to-markdown/helpers.d.ts +1 -0
  22. package/build/cjs/markup/codemirror/html-to-markdown/helpers.js +21 -0
  23. package/build/cjs/markup/commands/inline.js +18 -8
  24. package/build/cjs/utils/clipboard.d.ts +14 -0
  25. package/build/cjs/utils/clipboard.js +36 -1
  26. package/build/cjs/version.js +1 -1
  27. package/build/esm/bundle/Editor.js +1 -0
  28. package/build/esm/bundle/config/markup.d.ts +41 -17
  29. package/build/esm/bundle/config/markup.js +411 -307
  30. package/build/esm/bundle/config/wysiwyg.d.ts +29 -18
  31. package/build/esm/bundle/config/wysiwyg.js +499 -284
  32. package/build/esm/bundle/sticky/sticky.css +1 -1
  33. package/build/esm/bundle/types.d.ts +2 -0
  34. package/build/esm/extensions/behavior/Clipboard/utils.d.ts +1 -0
  35. package/build/esm/extensions/behavior/Clipboard/utils.js +1 -0
  36. package/build/esm/extensions/markdown/CodeBlock/handle-paste.js +2 -14
  37. package/build/esm/extensions/yfm/YfmFile/YfmFileSpecs/const.d.ts +12 -0
  38. package/build/esm/extensions/yfm/YfmFile/YfmFileSpecs/const.js +21 -2
  39. package/build/esm/extensions/yfm/YfmFile/YfmFileSpecs/index.d.ts +8 -1
  40. package/build/esm/extensions/yfm/YfmFile/YfmFileSpecs/index.js +29 -6
  41. package/build/esm/markup/codemirror/create.d.ts +1 -0
  42. package/build/esm/markup/codemirror/create.js +40 -3
  43. package/build/esm/markup/codemirror/html-to-markdown/converters.d.ts +111 -0
  44. package/build/esm/markup/codemirror/html-to-markdown/converters.js +210 -0
  45. package/build/esm/markup/codemirror/html-to-markdown/handlers.d.ts +104 -0
  46. package/build/esm/markup/codemirror/html-to-markdown/handlers.js +215 -0
  47. package/build/esm/markup/codemirror/html-to-markdown/helpers.d.ts +1 -0
  48. package/build/esm/markup/codemirror/html-to-markdown/helpers.js +17 -0
  49. package/build/esm/markup/commands/inline.js +18 -8
  50. package/build/esm/utils/clipboard.d.ts +14 -0
  51. package/build/esm/utils/clipboard.js +32 -0
  52. package/build/esm/version.js +1 -1
  53. package/build/styles.css +1 -1
  54. package/package.json +9 -7
@@ -15,7 +15,7 @@
15
15
  position: absolute;
16
16
  inset: var(--g-md-toolbar-sticky-inset, -4px);
17
17
  content: "";
18
- border: 1px solid var(--g-color-line-generic-solid);
18
+ border: var(--g-md-toolbar-sticky-border, 1px solid var(--g-color-line-generic-solid));
19
19
  border-radius: 4px;
20
20
  background-color: var(--g-color-base-background);
21
21
  }
@@ -109,6 +109,8 @@ export declare type MarkdownEditorMarkupConfig = {
109
109
  keymaps?: CreateCodemirrorParams['keymaps'];
110
110
  /** Overrides the default placeholder content. */
111
111
  placeholder?: CreateCodemirrorParams['placeholder'];
112
+ /** Enable HTML parsing when pasting content. */
113
+ parseHtmlOnPaste?: boolean;
112
114
  /**
113
115
  * Additional language data for markdown language in codemirror.
114
116
  * Can be used to configure additional autocompletions and others.
@@ -3,6 +3,7 @@ export declare enum DataTransferType {
3
3
  Text = "text/plain",
4
4
  Html = "text/html",
5
5
  Yfm = "text/yfm",
6
+ Rtf = "text/rtf",
6
7
  UriList = "text/uri-list",
7
8
  VSCodeData = "vscode-editor-data",
8
9
  Files = "Files"
@@ -7,6 +7,7 @@ var DataTransferType;
7
7
  DataTransferType["Text"] = "text/plain";
8
8
  DataTransferType["Html"] = "text/html";
9
9
  DataTransferType["Yfm"] = "text/yfm";
10
+ DataTransferType["Rtf"] = "text/rtf";
10
11
  DataTransferType["UriList"] = "text/uri-list";
11
12
  DataTransferType["VSCodeData"] = "vscode-editor-data";
12
13
  DataTransferType["Files"] = "Files";
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handlePaste = void 0;
4
- const utils_1 = require("../../behavior/Clipboard/utils");
4
+ const clipboard_1 = require("../../../utils/clipboard");
5
5
  const const_1 = require("./const");
6
6
  const handlePaste = (view, e) => {
7
7
  if (!e.clipboardData || view.state.selection.$from.parent.type.spec.code)
@@ -19,28 +19,16 @@ const handlePaste = (view, e) => {
19
19
  exports.handlePaste = handlePaste;
20
20
  function getCodeData(data) {
21
21
  var _a;
22
- if (data.getData(utils_1.DataTransferType.Text)) {
22
+ if (data.getData(clipboard_1.DataTransferType.Text)) {
23
23
  let editor = 'unknown';
24
24
  let mode;
25
- if (isVSCode(data)) {
25
+ if ((0, clipboard_1.isVSCode)(data)) {
26
26
  editor = 'vscode';
27
- mode = (_a = tryCatch(() => JSON.parse(data.getData(utils_1.DataTransferType.VSCodeData)))) === null || _a === void 0 ? void 0 : _a.mode;
27
+ mode = (_a = (0, clipboard_1.tryParseVSCodeData)(data)) === null || _a === void 0 ? void 0 : _a.mode;
28
28
  }
29
29
  else
30
30
  return null;
31
- return { editor, mode, value: data.getData(utils_1.DataTransferType.Text) };
31
+ return { editor, mode, value: data.getData(clipboard_1.DataTransferType.Text) };
32
32
  }
33
33
  return null;
34
34
  }
35
- function isVSCode(data) {
36
- return data.types.includes(utils_1.DataTransferType.VSCodeData);
37
- }
38
- function tryCatch(fn) {
39
- try {
40
- return fn();
41
- }
42
- catch (e) {
43
- console.error(e);
44
- }
45
- return undefined;
46
- }
@@ -1,5 +1,17 @@
1
+ import { FileHtmlAttr } from '@diplodoc/file-extension';
1
2
  import type { AttributeSpec } from 'prosemirror-model';
2
3
  export declare const yfmFileNodeName = "yfm_file";
4
+ export declare const YfmFileAttr: {
5
+ readonly Markup: "data-markup";
6
+ readonly Name: FileHtmlAttr.Download;
7
+ readonly Link: FileHtmlAttr.Href;
8
+ readonly ReferrerPolicy: FileHtmlAttr.ReferrerPolicy;
9
+ readonly Rel: FileHtmlAttr.Rel;
10
+ readonly Target: FileHtmlAttr.Target;
11
+ readonly Type: FileHtmlAttr.Type;
12
+ readonly Lang: FileHtmlAttr.HrefLang;
13
+ };
14
+ export declare const YFM_FILE_DIRECTIVE_ATTRS: readonly string[];
3
15
  export declare const KNOWN_ATTRS: readonly string[];
4
16
  export declare const REQUIRED_ATTRS: string[];
5
17
  export declare const fileNodeAttrsSpec: Record<string, AttributeSpec>;
@@ -1,8 +1,25 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.LINK_TO_FILE_ATTRS_MAP = exports.fileNodeAttrsSpec = exports.REQUIRED_ATTRS = exports.KNOWN_ATTRS = exports.yfmFileNodeName = void 0;
3
+ exports.LINK_TO_FILE_ATTRS_MAP = exports.fileNodeAttrsSpec = exports.REQUIRED_ATTRS = exports.KNOWN_ATTRS = exports.YFM_FILE_DIRECTIVE_ATTRS = exports.YfmFileAttr = exports.yfmFileNodeName = void 0;
4
4
  const file_extension_1 = require("@diplodoc/file-extension");
5
5
  exports.yfmFileNodeName = file_extension_1.FILE_TOKEN;
6
+ exports.YfmFileAttr = {
7
+ Markup: 'data-markup',
8
+ Name: file_extension_1.FileHtmlAttr.Download,
9
+ Link: file_extension_1.FileHtmlAttr.Href,
10
+ ReferrerPolicy: file_extension_1.FileHtmlAttr.ReferrerPolicy,
11
+ Rel: file_extension_1.FileHtmlAttr.Rel,
12
+ Target: file_extension_1.FileHtmlAttr.Target,
13
+ Type: file_extension_1.FileHtmlAttr.Type,
14
+ Lang: file_extension_1.FileHtmlAttr.HrefLang,
15
+ };
16
+ exports.YFM_FILE_DIRECTIVE_ATTRS = [
17
+ exports.YfmFileAttr.ReferrerPolicy,
18
+ exports.YfmFileAttr.Rel,
19
+ exports.YfmFileAttr.Target,
20
+ exports.YfmFileAttr.Type,
21
+ exports.YfmFileAttr.Lang,
22
+ ];
6
23
  exports.KNOWN_ATTRS = file_extension_1.FILE_KNOWN_ATTRS.map((attrName) => {
7
24
  if (attrName in file_extension_1.FILE_TO_LINK_ATTRS_MAP)
8
25
  return file_extension_1.FILE_TO_LINK_ATTRS_MAP[attrName];
@@ -13,7 +30,9 @@ exports.REQUIRED_ATTRS = file_extension_1.FILE_REQUIRED_ATTRS.map((attrName) =>
13
30
  return file_extension_1.FILE_TO_LINK_ATTRS_MAP[attrName];
14
31
  return attrName;
15
32
  });
16
- exports.fileNodeAttrsSpec = {};
33
+ exports.fileNodeAttrsSpec = {
34
+ [exports.YfmFileAttr.Markup]: { default: null },
35
+ };
17
36
  for (const attrName of exports.KNOWN_ATTRS) {
18
37
  const attrSpec = (exports.fileNodeAttrsSpec[attrName] = {});
19
38
  if (!exports.REQUIRED_ATTRS.includes(attrName)) {
@@ -1,4 +1,11 @@
1
1
  import type { Extension } from '../../../../core';
2
- export { yfmFileNodeName } from './const';
2
+ export { yfmFileNodeName, YfmFileAttr } from './const';
3
3
  export declare const fileType: (schema: import("prosemirror-model").Schema<any, any>) => import("prosemirror-model").NodeType;
4
+ declare global {
5
+ namespace MarkdownEditor {
6
+ interface DirectiveSyntaxAdditionalSupportedExtensions {
7
+ yfmFile: true;
8
+ }
9
+ }
10
+ }
4
11
  export declare const YfmFileSpecs: Extension;
@@ -1,14 +1,19 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.YfmFileSpecs = exports.fileType = exports.yfmFileNodeName = void 0;
3
+ exports.YfmFileSpecs = exports.fileType = exports.YfmFileAttr = exports.yfmFileNodeName = void 0;
4
4
  const file_extension_1 = require("@diplodoc/file-extension");
5
5
  const schema_1 = require("../../../../utils/schema");
6
6
  const const_1 = require("./const");
7
7
  var const_2 = require("./const");
8
8
  Object.defineProperty(exports, "yfmFileNodeName", { enumerable: true, get: function () { return const_2.yfmFileNodeName; } });
9
+ Object.defineProperty(exports, "YfmFileAttr", { enumerable: true, get: function () { return const_2.YfmFileAttr; } });
9
10
  exports.fileType = (0, schema_1.nodeTypeFactory)(const_1.yfmFileNodeName);
10
11
  const YfmFileSpecs = (builder) => {
11
- builder.configureMd((md) => md.use((0, file_extension_1.transform)({ bundle: false })));
12
+ const directiveContext = builder.context.get('directiveSyntax');
13
+ builder.configureMd((md) => md.use((0, file_extension_1.transform)({
14
+ bundle: false,
15
+ directiveSyntax: directiveContext === null || directiveContext === void 0 ? void 0 : directiveContext.mdPluginValueFor('yfmFile'),
16
+ })));
12
17
  builder.addNode(const_1.yfmFileNodeName, () => ({
13
18
  spec: {
14
19
  group: 'inline',
@@ -52,15 +57,20 @@ const YfmFileSpecs = (builder) => {
52
57
  name: const_1.yfmFileNodeName,
53
58
  type: 'node',
54
59
  getAttrs: (tok) => {
55
- var _a;
56
- return Object.fromEntries((_a = tok.attrs) !== null && _a !== void 0 ? _a : []);
60
+ const attrs = Object.fromEntries(tok.attrs || []);
61
+ attrs[const_1.YfmFileAttr.Markup] = tok.markup;
62
+ return attrs;
57
63
  },
58
64
  },
59
65
  },
60
66
  toMd: (state, node) => {
67
+ if (directiveContext === null || directiveContext === void 0 ? void 0 : directiveContext.shouldSerializeToDirective('yfmFile', node.attrs[const_1.YfmFileAttr.Markup])) {
68
+ state.write(serializeToDirective(node));
69
+ return;
70
+ }
61
71
  const attrsStr = Object.entries(node.attrs)
62
72
  .reduce((arr, [key, value]) => {
63
- if (value) {
73
+ if (key !== const_1.YfmFileAttr.Markup && value) {
64
74
  if (key in const_1.LINK_TO_FILE_ATTRS_MAP) {
65
75
  key = const_1.LINK_TO_FILE_ATTRS_MAP[key];
66
76
  }
@@ -74,3 +84,17 @@ const YfmFileSpecs = (builder) => {
74
84
  }));
75
85
  };
76
86
  exports.YfmFileSpecs = YfmFileSpecs;
87
+ function serializeToDirective(node) {
88
+ const filename = node.attrs[const_1.YfmFileAttr.Name] || '';
89
+ const filelink = node.attrs[const_1.YfmFileAttr.Link] || '';
90
+ let fileMarkup = `:file[${filename}](${filelink})`;
91
+ const attrs = const_1.YFM_FILE_DIRECTIVE_ATTRS.reduce((acc, key) => {
92
+ const value = node.attrs[key];
93
+ if (value)
94
+ acc.push(`${key}="${value}"`);
95
+ return acc;
96
+ }, []);
97
+ if (attrs.length)
98
+ fileMarkup += `{${attrs.join(' ')}}`;
99
+ return fileMarkup;
100
+ }
@@ -20,6 +20,7 @@ export declare type CreateCodemirrorParams = {
20
20
  onScroll: (event: Event) => void;
21
21
  reactRenderer: ReactRenderStorage;
22
22
  uploadHandler?: FileUploadHandler;
23
+ parseHtmlOnPaste?: boolean;
23
24
  parseInsertedUrlAsImage?: ParseInsertedUrlAsImage;
24
25
  needImageDimensions?: boolean;
25
26
  enableNewImageSizeCalculation?: boolean;
@@ -6,19 +6,20 @@ const commands_1 = require("@codemirror/commands");
6
6
  const language_1 = require("@codemirror/language");
7
7
  const view_1 = require("@codemirror/view");
8
8
  const action_names_1 = require("../../bundle/config/action-names");
9
- const utils_1 = require("../../extensions/behavior/Clipboard/utils");
10
9
  const logger_1 = require("../../logger");
11
10
  const shortcuts_1 = require("../../shortcuts");
11
+ const clipboard_1 = require("../../utils/clipboard");
12
12
  const commands_2 = require("../commands");
13
13
  const directive_facet_1 = require("./directive-facet");
14
14
  const files_upload_facet_1 = require("./files-upload-facet");
15
15
  const gravity_1 = require("./gravity");
16
+ const converters_1 = require("./html-to-markdown/converters");
16
17
  const pairing_chars_1 = require("./pairing-chars");
17
18
  const react_facet_1 = require("./react-facet");
18
19
  const plugin_1 = require("./search-plugin/plugin");
19
20
  const yfm_1 = require("./yfm");
20
21
  function createCodemirror(params) {
21
- const { doc, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, disabledExtensions = {}, keymaps = [], receiver, yfmLangOptions, extensions: extraExtensions, placeholder: placeholderContent, autocompletion: autocompletionConfig, parseInsertedUrlAsImage, directiveSyntax, } = params;
22
+ const { doc, reactRenderer, onCancel, onScroll, onSubmit, onChange, onDocChange, disabledExtensions = {}, keymaps = [], receiver, yfmLangOptions, extensions: extraExtensions, placeholder: placeholderContent, autocompletion: autocompletionConfig, parseHtmlOnPaste, parseInsertedUrlAsImage, directiveSyntax, } = params;
22
23
  const extensions = [gravity_1.gravityTheme, (0, view_1.placeholder)(placeholderContent)];
23
24
  if (!disabledExtensions.history) {
24
25
  extensions.push((0, commands_1.history)());
@@ -70,8 +71,44 @@ function createCodemirror(params) {
70
71
  },
71
72
  paste(event, editor) {
72
73
  var _a;
73
- if (event.clipboardData && parseInsertedUrlAsImage) {
74
- const { imageUrl, title } = parseInsertedUrlAsImage((_a = event.clipboardData.getData(utils_1.DataTransferType.Text)) !== null && _a !== void 0 ? _a : '') || {};
74
+ if (!event.clipboardData)
75
+ return;
76
+ // if clipboard contains YFM content - avoid any meddling with pasted content
77
+ // since text/yfm will contain valid markdown
78
+ const yfmContent = event.clipboardData.getData(clipboard_1.DataTransferType.Yfm);
79
+ if (yfmContent) {
80
+ event.preventDefault();
81
+ editor.dispatch(editor.state.replaceSelection(yfmContent));
82
+ return;
83
+ }
84
+ // checking if a copy buffer content is suitable for convertion
85
+ const shouldSkipHtml = (0, clipboard_1.shouldSkipHtmlConversion)(event.clipboardData);
86
+ // if we have text/html inside copy/paste buffer
87
+ const htmlContent = event.clipboardData.getData(clipboard_1.DataTransferType.Html);
88
+ // if we pasting markdown from VsCode we need skip html transformation
89
+ if (htmlContent && parseHtmlOnPaste && !shouldSkipHtml) {
90
+ let parsedMarkdownMarkup;
91
+ try {
92
+ const parser = new DOMParser();
93
+ const htmlDoc = parser.parseFromString(htmlContent, 'text/html');
94
+ const converter = new converters_1.MarkdownConverter();
95
+ parsedMarkdownMarkup = converter.processNode(htmlDoc.body).trim();
96
+ }
97
+ catch (e) {
98
+ // The code is pretty new and there might be random issues we haven't caught yet,
99
+ // especially with invalid HTML or weird DOM parsing errors.
100
+ // If something goes wrong, I just want to fall back to the "default pasting"
101
+ // rather than break the entire experience for the user.
102
+ logger_1.logger.error(e);
103
+ }
104
+ if (parsedMarkdownMarkup !== undefined) {
105
+ event.preventDefault();
106
+ editor.dispatch(editor.state.replaceSelection(parsedMarkdownMarkup));
107
+ return;
108
+ }
109
+ }
110
+ if (parseInsertedUrlAsImage) {
111
+ const { imageUrl, title } = parseInsertedUrlAsImage((_a = event.clipboardData.getData(clipboard_1.DataTransferType.Text)) !== null && _a !== void 0 ? _a : '') || {};
75
112
  if (!imageUrl) {
76
113
  return;
77
114
  }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Interface defining methods for visiting different types of HTML nodes.
3
+ * Each method corresponds to a specific HTML element type and returns its Markdown representation.
4
+ */
5
+ export interface HTMLNodeVisitor {
6
+ /** Converts a text node to Markdown */
7
+ visitText(node: Text): string;
8
+ /** Converts an anchor element to Markdown link syntax */
9
+ visitLink(node: HTMLAnchorElement): string;
10
+ /** Converts a header element to Markdown heading syntax */
11
+ visitHeader(node: HTMLElement, level: number): string;
12
+ /** Converts a paragraph element to Markdown */
13
+ visitParagraph(node: HTMLElement): string;
14
+ /** Converts formatting elements (bold, italic, etc.) to Markdown */
15
+ visitFormatting(node: HTMLElement): string;
16
+ /** Converts code elements to Markdown inline code syntax */
17
+ visitCode(node: HTMLElement): string;
18
+ /** Handles generic HTML elements with no specific Markdown conversion */
19
+ visitGeneric(node: HTMLElement): string;
20
+ /** Converts an HTML div element to Markdown format, adding a single newline */
21
+ visitDiv(node: HTMLElement): string;
22
+ /** Converts an HTML br element to a newline in Markdown */
23
+ visitBr(): string;
24
+ /** Converts a table row element to Markdown */
25
+ visitTableRow(node: HTMLTableRowElement): string;
26
+ /** Converts an HTML table element to Markdown table format */
27
+ visitTable(node: HTMLTableElement): string;
28
+ /** Convert img tag to Markdown image format */
29
+ visitImage(node: HTMLImageElement): string;
30
+ }
31
+ /**
32
+ * Main converter class that implements the visitor interface to convert HTML to Markdown.
33
+ * Uses the Chain of Responsibility pattern for handling different node types.
34
+ */
35
+ export declare class MarkdownConverter implements HTMLNodeVisitor {
36
+ private handler;
37
+ constructor();
38
+ /**
39
+ * Converts a text node to Markdown, escaping special characters.
40
+ */
41
+ visitText(node: Text): string;
42
+ /**
43
+ * Converts an HTML anchor element to Markdown link syntax.
44
+ */
45
+ visitLink(node: HTMLAnchorElement): string;
46
+ /**
47
+ * Converts an HTML heading element to Markdown heading syntax.
48
+ */
49
+ visitHeader(node: HTMLElement, level: number): string;
50
+ /**
51
+ * Converts an HTML paragraph to Markdown format.
52
+ */
53
+ visitParagraph(node: HTMLElement): string;
54
+ /**
55
+ * Applies Markdown formatting (bold, italic, etc.) to text content.
56
+ */
57
+ visitFormatting(node: HTMLElement): string;
58
+ /**
59
+ * Converts HTML code elements to Markdown inline code syntax.
60
+ */
61
+ visitCode(node: HTMLElement): string;
62
+ /**
63
+ * Handles generic HTML elements by processing their children.
64
+ */
65
+ visitGeneric(node: HTMLElement): string;
66
+ /**
67
+ * Converts an HTML div element to Markdown format, adding a single newline.
68
+ */
69
+ visitDiv(node: HTMLElement): string;
70
+ /**
71
+ * Converts an HTML br element to a newline in Markdown.
72
+ */
73
+ visitBr(): string;
74
+ /**
75
+ * Converts an HTML table row element to Markdown table row format.
76
+ */
77
+ visitTableRow(node: HTMLTableRowElement): string;
78
+ /**
79
+ * Converts an HTML table element to Markdown table format.
80
+ */
81
+ visitTable(node: HTMLTableElement): string;
82
+ /**
83
+ * Converts img tag to Markdown image format
84
+ */
85
+ visitImage(node: HTMLImageElement): string;
86
+ /**
87
+ * Processes a single node using the handler chain.
88
+ */
89
+ processNode(node: Node): string;
90
+ /**
91
+ * Creates and links together handlers in a specific order implementing the Chain of Responsibility pattern.
92
+ * @returns The first handler in the chain
93
+ */
94
+ private setupHandlerChain;
95
+ /**
96
+ * Recursively collects and processes text content from a node and its children.
97
+ */
98
+ private collectTextContent;
99
+ /**
100
+ * Collects raw text content from code elements.
101
+ */
102
+ private collectCodeContent;
103
+ /**
104
+ * Processes all child nodes of a given node.
105
+ */
106
+ private processChildren;
107
+ /**
108
+ * Gets the first handler in the chain.
109
+ */
110
+ private getHandler;
111
+ }
@@ -0,0 +1,214 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MarkdownConverter = void 0;
4
+ const handlers_1 = require("./handlers");
5
+ const helpers_1 = require("./helpers");
6
+ /**
7
+ * Main converter class that implements the visitor interface to convert HTML to Markdown.
8
+ * Uses the Chain of Responsibility pattern for handling different node types.
9
+ */
10
+ class MarkdownConverter {
11
+ constructor() {
12
+ // Set up the chain of responsibility for handling different node types
13
+ this.handler = this.setupHandlerChain();
14
+ }
15
+ /**
16
+ * Converts a text node to Markdown, escaping special characters.
17
+ */
18
+ visitText(node) {
19
+ return (node.textContent || '').replace(/\n+/g, '').replace(/([<>])/g, '\\$1');
20
+ }
21
+ /**
22
+ * Converts an HTML anchor element to Markdown link syntax.
23
+ */
24
+ visitLink(node) {
25
+ var _a;
26
+ const linkText = this.collectTextContent(node);
27
+ const url = node.href || '';
28
+ // Handle links with formatted content vs plain text differently
29
+ const formattedText = node.childNodes.length === 1 && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.nodeType) === Node.TEXT_NODE
30
+ ? (0, helpers_1.applyFormatting)(linkText, node) // Plain text link
31
+ : Array.from(node.childNodes)
32
+ .map((child) => {
33
+ if (child.nodeType === Node.ELEMENT_NODE) {
34
+ return (0, helpers_1.applyFormatting)(child.textContent || '', child);
35
+ }
36
+ return child.textContent || '';
37
+ })
38
+ .join(''); // Apply formatting for each formatted child node
39
+ return `[${formattedText}](${url} "${linkText.replace(/"/g, '\\"')}")`;
40
+ }
41
+ /**
42
+ * Converts an HTML heading element to Markdown heading syntax.
43
+ */
44
+ visitHeader(node, level) {
45
+ const headerContent = this.collectTextContent(node);
46
+ return '#'.repeat(level) + ' ' + headerContent + '\n';
47
+ }
48
+ /**
49
+ * Converts an HTML paragraph to Markdown format.
50
+ */
51
+ visitParagraph(node) {
52
+ const content = this.processChildren(node);
53
+ return content.trim() + '\n\n';
54
+ }
55
+ /**
56
+ * Applies Markdown formatting (bold, italic, etc.) to text content.
57
+ */
58
+ visitFormatting(node) {
59
+ var _a;
60
+ if (node.childNodes.length === 1 && ((_a = node.firstChild) === null || _a === void 0 ? void 0 : _a.nodeType) === Node.TEXT_NODE) {
61
+ const text = this.collectTextContent(node);
62
+ return (0, helpers_1.applyFormatting)(text, node);
63
+ }
64
+ return (0, helpers_1.applyFormatting)(this.visitGeneric(node), node);
65
+ }
66
+ /**
67
+ * Converts HTML code elements to Markdown inline code syntax.
68
+ */
69
+ visitCode(node) {
70
+ const codeContent = this.collectCodeContent(node);
71
+ if (codeContent.includes('\n')) {
72
+ return '```\n' + codeContent + '\n```\n';
73
+ }
74
+ else if (codeContent.includes('`')) {
75
+ return '`` ' + codeContent + ' ``';
76
+ }
77
+ else {
78
+ return `\`${codeContent}\``;
79
+ }
80
+ }
81
+ /**
82
+ * Handles generic HTML elements by processing their children.
83
+ */
84
+ visitGeneric(node) {
85
+ return this.processChildren(node);
86
+ }
87
+ /**
88
+ * Converts an HTML div element to Markdown format, adding a single newline.
89
+ */
90
+ visitDiv(node) {
91
+ const content = this.processChildren(node);
92
+ return content + '\n'; // Add a single newline for <div>
93
+ }
94
+ /**
95
+ * Converts an HTML br element to a newline in Markdown.
96
+ */
97
+ visitBr() {
98
+ return '\n'; // Single newline for <br>
99
+ }
100
+ /**
101
+ * Converts an HTML table row element to Markdown table row format.
102
+ */
103
+ visitTableRow(node) {
104
+ const cells = Array.from(node.children).map((cell) => {
105
+ return this.visitGeneric(cell).trim() || '';
106
+ });
107
+ return '||\n' + cells.join('\n|\n') + '\n||';
108
+ }
109
+ /**
110
+ * Converts an HTML table element to Markdown table format.
111
+ */
112
+ visitTable(node) {
113
+ const rows = [];
114
+ const tableRows = Array.from(node.querySelectorAll('tr'));
115
+ tableRows.forEach((row) => {
116
+ rows.push(this.visitTableRow(row));
117
+ });
118
+ return '\n\n#|\n' + rows.join('\n') + '\n|#\n\n';
119
+ }
120
+ /**
121
+ * Converts img tag to Markdown image format
122
+ */
123
+ visitImage(node) {
124
+ const imgElement = node;
125
+ const altText = imgElement.alt || '';
126
+ const src = imgElement.src || '';
127
+ return `![${altText}](${src})`;
128
+ }
129
+ /**
130
+ * Processes a single node using the handler chain.
131
+ */
132
+ processNode(node) {
133
+ const result = this.getHandler().handle(node, this);
134
+ return result;
135
+ }
136
+ /**
137
+ * Creates and links together handlers in a specific order implementing the Chain of Responsibility pattern.
138
+ * @returns The first handler in the chain
139
+ */
140
+ setupHandlerChain() {
141
+ // Create handlers for each type of node
142
+ const textHandler = new handlers_1.TextNodeHandler();
143
+ const linkHandler = new handlers_1.LinkHandler();
144
+ const headerHandler = new handlers_1.HeaderHandler();
145
+ const paragraphHandler = new handlers_1.ParagraphHandler();
146
+ const formattingHandler = new handlers_1.FormattingHandler();
147
+ const codeHandler = new handlers_1.CodeHandler();
148
+ const genericHandler = new handlers_1.GenericHandler();
149
+ const orderedListHandler = new handlers_1.OrderedListHandler();
150
+ const unorderedListHandler = new handlers_1.UnorderedListHandler();
151
+ const divHandler = new handlers_1.DivHandler();
152
+ const brHandler = new handlers_1.BrHandler();
153
+ const tableRowHandler = new handlers_1.TableRowHandler();
154
+ const tableHandler = new handlers_1.TableHandler();
155
+ const imageHandler = new handlers_1.ImageHandler(); // New handler for <img>
156
+ // Chain handlers together in priority order
157
+ textHandler
158
+ .setNext(linkHandler)
159
+ .setNext(headerHandler)
160
+ .setNext(paragraphHandler)
161
+ .setNext(divHandler)
162
+ .setNext(brHandler)
163
+ .setNext(orderedListHandler)
164
+ .setNext(unorderedListHandler)
165
+ .setNext(formattingHandler)
166
+ .setNext(codeHandler)
167
+ .setNext(imageHandler) // Add image handler
168
+ .setNext(tableHandler)
169
+ .setNext(tableRowHandler)
170
+ .setNext(genericHandler);
171
+ return textHandler;
172
+ }
173
+ /**
174
+ * Recursively collects and processes text content from a node and its children.
175
+ */
176
+ collectTextContent(node) {
177
+ // handle seo elements (hide it's content)
178
+ if (node.className === 'visually-hidden') {
179
+ return '';
180
+ }
181
+ if (node.nodeType === Node.TEXT_NODE) {
182
+ return this.visitText(node);
183
+ }
184
+ return Array.from(node.childNodes)
185
+ .map((child) => this.collectTextContent(child))
186
+ .join('');
187
+ }
188
+ /**
189
+ * Collects raw text content from code elements.
190
+ */
191
+ collectCodeContent(node) {
192
+ if (node.nodeType === Node.TEXT_NODE) {
193
+ return node.textContent || '';
194
+ }
195
+ return Array.from(node.childNodes)
196
+ .map((child) => this.collectCodeContent(child))
197
+ .join('');
198
+ }
199
+ /**
200
+ * Processes all child nodes of a given node.
201
+ */
202
+ processChildren(node) {
203
+ return Array.from(node.childNodes)
204
+ .map((child) => this.processNode(child))
205
+ .join('');
206
+ }
207
+ /**
208
+ * Gets the first handler in the chain.
209
+ */
210
+ getHandler() {
211
+ return this.handler;
212
+ }
213
+ }
214
+ exports.MarkdownConverter = MarkdownConverter;