feat(ringcentral): add markdown conversion for RingCentral Mini-Markdown
RingCentral uses a different markdown format where _text_ means underline, not italic. This adds automatic conversion: - _italic_ -> *italic* (preserve italic intent) - __bold__ -> **bold** - Remove unsupported: ~~strikethrough~~, `code`, ```code blocks``` - # Heading -> **Heading** (bold) - Preserve URLs in links from underscore conversion Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
This commit is contained in:
parent
dba1763c81
commit
0b1aebe80f
@ -1,5 +1,6 @@
|
||||
import type { ResolvedRingCentralAccount } from "./accounts.js";
|
||||
import { getRingCentralPlatform } from "./auth.js";
|
||||
import { toRingCentralMarkdown } from "./markdown.js";
|
||||
import type {
|
||||
RingCentralChat,
|
||||
RingCentralPost,
|
||||
@ -20,7 +21,7 @@ export async function sendRingCentralMessage(params: {
|
||||
const platform = await getRingCentralPlatform(account);
|
||||
|
||||
const body: Record<string, unknown> = {};
|
||||
if (text) body.text = text;
|
||||
if (text) body.text = toRingCentralMarkdown(text);
|
||||
if (attachments && attachments.length > 0) {
|
||||
body.attachments = attachments;
|
||||
}
|
||||
@ -41,7 +42,7 @@ export async function updateRingCentralMessage(params: {
|
||||
|
||||
const response = await platform.patch(
|
||||
`${TM_API_BASE}/chats/${chatId}/posts/${postId}`,
|
||||
{ text },
|
||||
{ text: toRingCentralMarkdown(text) },
|
||||
);
|
||||
const result = (await response.json()) as RingCentralPost;
|
||||
return { postId: result.id };
|
||||
|
||||
121
extensions/ringcentral/src/markdown.test.ts
Normal file
121
extensions/ringcentral/src/markdown.test.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { toRingCentralMarkdown, needsMarkdownConversion } from "./markdown.js";
|
||||
|
||||
describe("toRingCentralMarkdown", () => {
|
||||
it("converts single underscore italic to asterisk", () => {
|
||||
expect(toRingCentralMarkdown("_italic_")).toBe("*italic*");
|
||||
expect(toRingCentralMarkdown("This is _italic_ text")).toBe("This is *italic* text");
|
||||
});
|
||||
|
||||
it("preserves double asterisk bold", () => {
|
||||
expect(toRingCentralMarkdown("**bold**")).toBe("**bold**");
|
||||
expect(toRingCentralMarkdown("This is **bold** text")).toBe("This is **bold** text");
|
||||
});
|
||||
|
||||
it("converts double underscore to bold", () => {
|
||||
expect(toRingCentralMarkdown("__bold__")).toBe("**bold**");
|
||||
});
|
||||
|
||||
it("removes strikethrough", () => {
|
||||
expect(toRingCentralMarkdown("~~strikethrough~~")).toBe("strikethrough");
|
||||
expect(toRingCentralMarkdown("This is ~~deleted~~ text")).toBe("This is deleted text");
|
||||
});
|
||||
|
||||
it("removes code blocks", () => {
|
||||
expect(toRingCentralMarkdown("```\ncode\n```")).toBe("code");
|
||||
expect(toRingCentralMarkdown("```javascript\nconst x = 1;\n```")).toBe("const x = 1;");
|
||||
});
|
||||
|
||||
it("removes inline code", () => {
|
||||
expect(toRingCentralMarkdown("`code`")).toBe("code");
|
||||
expect(toRingCentralMarkdown("Use `npm install` to install")).toBe("Use npm install to install");
|
||||
});
|
||||
|
||||
it("converts headings to bold", () => {
|
||||
expect(toRingCentralMarkdown("# Heading 1")).toBe("**Heading 1**");
|
||||
expect(toRingCentralMarkdown("## Heading 2")).toBe("**Heading 2**");
|
||||
expect(toRingCentralMarkdown("### Heading 3")).toBe("**Heading 3**");
|
||||
});
|
||||
|
||||
it("normalizes bullet lists", () => {
|
||||
expect(toRingCentralMarkdown("- item")).toBe("* item");
|
||||
expect(toRingCentralMarkdown("+ item")).toBe("* item");
|
||||
expect(toRingCentralMarkdown("* item")).toBe("* item");
|
||||
});
|
||||
|
||||
it("preserves links", () => {
|
||||
expect(toRingCentralMarkdown("[link](https://example.com)")).toBe("[link](https://example.com)");
|
||||
});
|
||||
|
||||
it("preserves blockquotes", () => {
|
||||
expect(toRingCentralMarkdown("> quote")).toBe("> quote");
|
||||
});
|
||||
|
||||
it("preserves numbered lists", () => {
|
||||
expect(toRingCentralMarkdown("1. first\n2. second")).toBe("1. first\n2. second");
|
||||
});
|
||||
|
||||
it("handles complex markdown", () => {
|
||||
const input = `# Title
|
||||
|
||||
This is _italic_ and **bold** text.
|
||||
|
||||
- Item 1
|
||||
- Item 2
|
||||
|
||||
\`\`\`
|
||||
code block
|
||||
\`\`\`
|
||||
|
||||
Use \`inline code\` here.`;
|
||||
|
||||
const expected = `**Title**
|
||||
|
||||
This is *italic* and **bold** text.
|
||||
|
||||
* Item 1
|
||||
* Item 2
|
||||
|
||||
code block
|
||||
|
||||
Use inline code here.`;
|
||||
|
||||
expect(toRingCentralMarkdown(input)).toBe(expected);
|
||||
});
|
||||
|
||||
it("does not convert underscore in URLs or paths", () => {
|
||||
// URLs with underscores should be preserved in links
|
||||
expect(toRingCentralMarkdown("[link](https://example.com/path_with_underscore)"))
|
||||
.toBe("[link](https://example.com/path_with_underscore)");
|
||||
});
|
||||
});
|
||||
|
||||
describe("needsMarkdownConversion", () => {
|
||||
it("returns true for text with underscore italic", () => {
|
||||
expect(needsMarkdownConversion("_italic_")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for text with strikethrough", () => {
|
||||
expect(needsMarkdownConversion("~~strike~~")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for text with code blocks", () => {
|
||||
expect(needsMarkdownConversion("```code```")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for text with inline code", () => {
|
||||
expect(needsMarkdownConversion("`code`")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns true for text with headings", () => {
|
||||
expect(needsMarkdownConversion("# Heading")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for plain text", () => {
|
||||
expect(needsMarkdownConversion("plain text")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for already compatible markdown", () => {
|
||||
expect(needsMarkdownConversion("*italic* and **bold**")).toBe(false);
|
||||
});
|
||||
});
|
||||
87
extensions/ringcentral/src/markdown.ts
Normal file
87
extensions/ringcentral/src/markdown.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* Convert standard Markdown to RingCentral Mini-Markdown format.
|
||||
*
|
||||
* RingCentral Mini-Markdown differences:
|
||||
* - `_text_` = underline (not italic)
|
||||
* - `*text*` = italic
|
||||
* - `**text**` = bold
|
||||
* - `[text](url)` = link
|
||||
* - `> quote` = blockquote
|
||||
* - `* item` = bullet list
|
||||
*
|
||||
* Not supported: strikethrough (~~), code blocks (```), headings (#), etc.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert standard markdown to RingCentral mini-markdown
|
||||
*/
|
||||
export function toRingCentralMarkdown(text: string): string {
|
||||
let result = text;
|
||||
|
||||
// 1. Preserve links - temporarily replace them to avoid modifying URLs
|
||||
const linkPlaceholders: string[] = [];
|
||||
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (match) => {
|
||||
const idx = linkPlaceholders.length;
|
||||
linkPlaceholders.push(match);
|
||||
return `\x00LINK_${idx}\x00`;
|
||||
});
|
||||
|
||||
// 2. Convert __text__ (some markdown bold) to **text**
|
||||
result = result.replace(/__([^_]+)__/g, "**$1**");
|
||||
|
||||
// 3. Convert single _text_ to *text* for italic
|
||||
// Match _text_ but not inside words (e.g., snake_case_name)
|
||||
result = result.replace(/(?<=^|[\s\p{P}])_([^_\n]+)_(?=$|[\s\p{P}])/gu, "*$1*");
|
||||
|
||||
// 4. Remove strikethrough ~~text~~ (not supported)
|
||||
result = result.replace(/~~([^~]+)~~/g, "$1");
|
||||
|
||||
// 5. Convert code blocks to plain text (not well supported)
|
||||
// Remove triple backtick code blocks
|
||||
result = result.replace(/```[\s\S]*?```/g, (match) => {
|
||||
// Extract content without the backticks and language identifier
|
||||
const content = match.replace(/```\w*\n?/, "").replace(/\n?```$/, "");
|
||||
return content;
|
||||
});
|
||||
|
||||
// Convert inline code `text` to plain text
|
||||
result = result.replace(/`([^`]+)`/g, "$1");
|
||||
|
||||
// 6. Convert headings to bold (# Heading -> **Heading**)
|
||||
result = result.replace(/^#{1,6}\s+(.+)$/gm, "**$1**");
|
||||
|
||||
// 7. Convert horizontal rules (---, ***, ___) to simple separator
|
||||
result = result.replace(/^[-*_]{3,}$/gm, "---");
|
||||
|
||||
// 8. Normalize bullet lists (-, +, *) to * for consistency
|
||||
result = result.replace(/^(\s*)[-+]\s+/gm, "$1* ");
|
||||
|
||||
// 9. Restore links
|
||||
result = result.replace(/\x00LINK_(\d+)\x00/g, (_match, idx) => {
|
||||
return linkPlaceholders[Number(idx)];
|
||||
});
|
||||
|
||||
// 10. Keep numbered lists as-is (1. item)
|
||||
// 11. Keep blockquotes as-is (> quote)
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if text contains markdown that needs conversion
|
||||
*/
|
||||
export function needsMarkdownConversion(text: string): boolean {
|
||||
// Check for patterns that need conversion
|
||||
return (
|
||||
// Single underscore italic
|
||||
/(?<![\\*_])_[^_\n]+_(?![_])/.test(text) ||
|
||||
// Strikethrough
|
||||
/~~[^~]+~~/.test(text) ||
|
||||
// Code blocks
|
||||
/```[\s\S]*?```/.test(text) ||
|
||||
// Inline code
|
||||
/`[^`]+`/.test(text) ||
|
||||
// Headings
|
||||
/^#{1,6}\s+.+$/m.test(text)
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user