Files
jiang13-forum/frontend/src/utils/markdownFormat.ts
2026-08-05 12:44:00 +08:00

123 lines
4.1 KiB
TypeScript

import { applyTextareaChange } from './markdownIndent';
type ChangeHandler = (value: string) => void;
/** 在选区两侧包裹 Markdown 标记 */
export function wrapMarkdownSelection(
textarea: HTMLTextAreaElement,
value: string,
prefix: string,
suffix: string,
placeholder: string,
onChange: ChangeHandler,
) {
const { selectionStart, selectionEnd } = textarea;
const selected = value.slice(selectionStart, selectionEnd);
const text = selected || placeholder;
const insert = prefix + text + suffix;
const next = value.slice(0, selectionStart) + insert + value.slice(selectionEnd);
const newStart = selectionStart + prefix.length;
const newEnd = newStart + text.length;
applyTextareaChange(textarea, next, newStart, newEnd, onChange);
}
/** 为当前行或选区行添加前缀(如引用) */
export function prefixMarkdownLines(
textarea: HTMLTextAreaElement,
value: string,
prefix: string,
onChange: ChangeHandler,
) {
const { selectionStart, selectionEnd } = textarea;
const lineStart = value.lastIndexOf('\n', selectionStart - 1) + 1;
const nextNewline = value.indexOf('\n', selectionEnd);
const lineEnd = nextNewline === -1 ? value.length : nextNewline;
const block = value.slice(lineStart, lineEnd);
const lines = block.split('\n').map(line => `${prefix}${line}`);
const next = value.slice(0, lineStart) + lines.join('\n') + value.slice(lineEnd);
applyTextareaChange(
textarea,
next,
selectionStart + prefix.length,
selectionEnd + prefix.length * lines.length,
onChange,
);
}
/** 切换当前行标题级别(源码模式) */
export function cycleMarkdownHeading(
textarea: HTMLTextAreaElement,
value: string,
onChange: ChangeHandler,
) {
const { selectionStart } = textarea;
const lineStart = value.lastIndexOf('\n', selectionStart - 1) + 1;
const lineEnd = value.indexOf('\n', selectionStart);
const end = lineEnd === -1 ? value.length : lineEnd;
const line = value.slice(lineStart, end);
const match = line.match(/^(#{2,6})\s+(.*)$/);
let nextLine: string;
if (!match) {
nextLine = `## ${line}`;
} else {
const level = match[1].length;
const body = match[2];
nextLine = level >= 6 ? body : `${'#'.repeat(level + 1)} ${body}`;
}
const next = value.slice(0, lineStart) + nextLine + value.slice(end);
const offset = nextLine.length - line.length;
applyTextareaChange(
textarea,
next,
selectionStart + Math.max(0, offset),
selectionStart + Math.max(0, offset),
onChange,
);
}
/** 插入登录可见区块模板 */
export function insertMarkdownMembersOnly(
textarea: HTMLTextAreaElement,
value: string,
onChange: ChangeHandler,
) {
const { selectionStart, selectionEnd } = textarea;
const snippet = '\n\n<members-only data-gate="login">\n\n\n</members-only>\n\n';
const next = value.slice(0, selectionStart) + snippet + value.slice(selectionEnd);
const cursor = selectionStart + '\n\n<members-only data-gate="login">\n\n'.length;
applyTextareaChange(textarea, next, cursor, cursor, onChange);
}
/** 插入回复可见区块模板 */
export function insertMarkdownReplyOnly(
textarea: HTMLTextAreaElement,
value: string,
onChange: ChangeHandler,
) {
const { selectionStart, selectionEnd } = textarea;
const snippet = '\n\n<reply-only data-gate="reply">\n\n\n</reply-only>\n\n';
const next = value.slice(0, selectionStart) + snippet + value.slice(selectionEnd);
const cursor = selectionStart + '\n\n<reply-only data-gate="reply">\n\n'.length;
applyTextareaChange(textarea, next, cursor, cursor, onChange);
}
/** 在光标处插入链接 Markdown */
export function insertMarkdownLink(
textarea: HTMLTextAreaElement,
value: string,
url: string,
onChange: ChangeHandler,
) {
const { selectionStart, selectionEnd } = textarea;
const selected = value.slice(selectionStart, selectionEnd) || '链接文字';
const insert = `[${selected}](${url})`;
const next = value.slice(0, selectionStart) + insert + value.slice(selectionEnd);
applyTextareaChange(
textarea,
next,
selectionStart + insert.length,
selectionStart + insert.length,
onChange,
);
}