Something went wrong. Try again.
Monorepo for Tangled
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204export interface Selection { value: string; start: number; end: number;}
export interface Edit { from: number; to: number; insert: string; selectionStart: number; selectionEnd: number;}
export type ListKind = "bullet" | "ordered" | "task";
const URL_PATTERN = /^https?:\/\/\S+$/;
export const wrap = ({ value, start, end }: Selection, marker: string): Edit => { const selected = value.slice(start, end); const before = value.slice(start - marker.length, start); const after = value.slice(end, end + marker.length); if (before === marker && after === marker) { return { from: start - marker.length, to: end + marker.length, insert: selected, selectionStart: start - marker.length, selectionEnd: end - marker.length }; } if ( selected.length >= marker.length * 2 && selected.startsWith(marker) && selected.endsWith(marker) ) { const inner = selected.slice(marker.length, -marker.length); return { from: start, to: end, insert: inner, selectionStart: start, selectionEnd: start + inner.length }; } return { from: start, to: end, insert: `${marker}${selected}${marker}`, selectionStart: start + marker.length, selectionEnd: end + marker.length };};
const blockPadding = (value: string, from: number, to: number) => { const before = value.slice(0, from); const after = value.slice(to); const leading = before === "" || before.endsWith("\n\n") ? "" : before.endsWith("\n") ? "\n" : "\n\n"; const trailing = after === "" || after.startsWith("\n\n") ? "" : after.startsWith("\n") ? "\n" : "\n\n"; return { leading, trailing };};
const insertBlock = ( { value, start, end }: Selection, block: string, select: [number, number]): Edit => { const { leading, trailing } = blockPadding(value, start, end); const offset = start + leading.length; return { from: start, to: end, insert: `${leading}${block}${trailing}`, selectionStart: offset + select[0], selectionEnd: offset + select[1] };};
export const code = (selection: Selection): Edit => selection.value.slice(selection.start, selection.end).includes("\n") ? codeBlock(selection) : wrap(selection, "`");
export const codeBlock = (selection: Selection): Edit => { const selected = selection.value.slice(selection.start, selection.end); return insertBlock(selection, `\`\`\`\n${selected}\n\`\`\``, [4, 4 + selected.length]);};
export const link = ({ value, start, end }: Selection): Edit => { const selected = value.slice(start, end); if (URL_PATTERN.test(selected)) { return { from: start, to: end, insert: `[](${selected})`, selectionStart: start + 1, selectionEnd: start + 1 }; } const urlStart = start + selected.length + 3; return { from: start, to: end, insert: `[${selected}](url)`, selectionStart: urlStart, selectionEnd: urlStart + 3 };};
export const image = ({ value, start, end }: Selection): Edit => { const selected = value.slice(start, end); const urlStart = start + selected.length + 4; return { from: start, to: end, insert: ``, selectionStart: urlStart, selectionEnd: urlStart + 3 };};
export const separator = (selection: Selection): Edit => insertBlock({ ...selection, start: selection.end }, "---", [3, 3]);
export const table = (selection: Selection): Edit => insertBlock( { ...selection, start: selection.end }, "| Column | Column |\n| ------ | ------ |\n| Cell | Cell |", [2, 8] );
const LIST_PREFIX: Record<ListKind, (index: number) => string> = { bullet: () => "- ", ordered: (index) => `${index + 1}. `, task: () => "- [ ] "};
const LIST_MATCH: Record<ListKind, RegExp> = { bullet: /^- (?!\[[ xX]\] )/, ordered: /^\d+\. /, task: /^- \[[ xX]\] /};
export const list = ({ value, start, end }: Selection, kind: ListKind): Edit => { const from = value.lastIndexOf("\n", start - 1) + 1; const lineEnd = value.indexOf("\n", end); const to = lineEnd === -1 ? value.length : lineEnd; const lines = value.slice(from, to).split("\n"); const listed = lines.every((line) => LIST_MATCH[kind].test(line)); const next = lines.map((line, index) => listed ? line.replace(LIST_MATCH[kind], "") : `${LIST_PREFIX[kind](index)}${line}` ); const insert = next.join("\n"); if (lines.length === 1) { const shift = insert.length - lines[0].length; return { from, to, insert, selectionStart: Math.max(from, start + shift), selectionEnd: Math.max(from, end + shift) }; } return { from, to, insert, selectionStart: from, selectionEnd: from + insert.length };};
export interface Trigger { char: "@" | "#"; query: string; from: number; to: number;}
const TRIGGER_PATTERN = /(?:^|[\s(])([@#])([\w.-]*)$/;
export const findTrigger = (value: string, caret: number): Trigger | null => { const lineStart = value.lastIndexOf("\n", caret - 1) + 1; const match = TRIGGER_PATTERN.exec(value.slice(lineStart, caret)); if (!match) return null; const query = match[2]; return { char: match[1] as Trigger["char"], query, from: caret - query.length - 1, to: caret };};
export const applyEdit = (textarea: HTMLTextAreaElement, edit: Edit) => { textarea.focus(); textarea.setSelectionRange(edit.from, edit.to); const applied = edit.insert === "" ? document.execCommand("delete") : document.execCommand("insertText", false, edit.insert); if (!applied) { textarea.setRangeText(edit.insert, edit.from, edit.to, "end"); textarea.dispatchEvent(new Event("input", { bubbles: true })); } textarea.setSelectionRange(edit.selectionStart, edit.selectionEnd);};