refactor: Naming (#27001)
This commit is contained in:
+43
-39
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* Maps between the chat-form contenteditable's markdown source and the
|
||||
* Maps between the chat-form-input-rich's markdown source and the
|
||||
* badge/code/text token stream the DOM is built from. A badge is one
|
||||
* opaque source contribution (`[name](file://path)`); its own subtree
|
||||
* is never walked, and the caret cannot land inside it, so offsets
|
||||
@@ -35,10 +35,10 @@ import {
|
||||
MENTION_BADGE_SVG_ATTRIBUTES,
|
||||
SETTINGS_KEYS
|
||||
} from '$lib/constants';
|
||||
import { ContentEditableTokenKind } from '$lib/enums';
|
||||
import { ChatFormInputRichTokenKind } from '$lib/enums';
|
||||
import { settingsStore } from '$lib/stores/settings.svelte';
|
||||
import { toolsStore } from '$lib/stores/tools.svelte';
|
||||
import type { ContentEditableToken } from '$lib/types/contenteditable';
|
||||
import type { ChatFormInputRichToken } from '$lib/types/chat-form-input-rich';
|
||||
|
||||
// Block wrappers browsers insert for newlines; each folds back into a
|
||||
// single `\n` during serialization.
|
||||
@@ -66,7 +66,7 @@ const CODE_SPAN_RE = /(```[\s\S]*?```)|(`[^`\n]+`)/g;
|
||||
/**
|
||||
* Cheap gate check for `ChatForm`: does the buffer contain a
|
||||
* complete code span (inline or fenced)? Used to promote the plain
|
||||
* textarea to the contenteditable renderer.
|
||||
* textarea to the chat-form-input-rich renderer.
|
||||
*/
|
||||
export function containsCodeSpan(value: string): boolean {
|
||||
CODE_SPAN_RE.lastIndex = 0;
|
||||
@@ -102,14 +102,14 @@ export function isOffsetInCodeBlock(source: string, offset: number): boolean {
|
||||
|
||||
/**
|
||||
* Tokenize a markdown source value into the segments the
|
||||
* contenteditable will render. Code spans are carved out first
|
||||
* chat-form-input-rich will render. Code spans are carved out first
|
||||
* (their content is literal - a `file://` link inside backticks
|
||||
* must NOT render as a badge), then plain text and badges
|
||||
* interleave in the remaining gaps. Any whitespace after a badge
|
||||
* stays in a plain text token so the round trip is byte-exact.
|
||||
*/
|
||||
export function tokenizeContent(input: string): ContentEditableToken[] {
|
||||
const tokens: ContentEditableToken[] = [];
|
||||
export function tokenizeContent(input: string): ChatFormInputRichToken[] {
|
||||
const tokens: ChatFormInputRichToken[] = [];
|
||||
|
||||
let cursor = 0;
|
||||
|
||||
@@ -126,8 +126,8 @@ export function tokenizeContent(input: string): ContentEditableToken[] {
|
||||
|
||||
tokens.push(
|
||||
match[1] !== undefined
|
||||
? { kind: ContentEditableTokenKind.CODE_BLOCK, text: match[1] }
|
||||
: { kind: ContentEditableTokenKind.INLINE_CODE, text: match[2] }
|
||||
? { kind: ChatFormInputRichTokenKind.CODE_BLOCK, text: match[1] }
|
||||
: { kind: ChatFormInputRichTokenKind.CODE_INLINE, text: match[2] }
|
||||
);
|
||||
cursor = start + match[0].length;
|
||||
}
|
||||
@@ -142,7 +142,7 @@ export function tokenizeContent(input: string): ContentEditableToken[] {
|
||||
/**
|
||||
* Tokenize a code-free segment into text and badge tokens.
|
||||
*/
|
||||
function pushTextAndBadgeTokens(input: string, tokens: ContentEditableToken[]) {
|
||||
function pushTextAndBadgeTokens(input: string, tokens: ChatFormInputRichToken[]) {
|
||||
let cursor = 0;
|
||||
|
||||
MENTION_BADGE_RE.lastIndex = 0;
|
||||
@@ -154,24 +154,26 @@ function pushTextAndBadgeTokens(input: string, tokens: ContentEditableToken[]) {
|
||||
const start = match.index;
|
||||
|
||||
if (start > cursor) {
|
||||
tokens.push({ kind: ContentEditableTokenKind.TEXT, text: input.slice(cursor, start) });
|
||||
tokens.push({ kind: ChatFormInputRichTokenKind.TEXT, text: input.slice(cursor, start) });
|
||||
}
|
||||
|
||||
tokens.push({ kind: ContentEditableTokenKind.BADGE, name, path });
|
||||
tokens.push({ kind: ChatFormInputRichTokenKind.BADGE, name, path });
|
||||
cursor = start + whole.length;
|
||||
}
|
||||
|
||||
if (cursor < input.length) {
|
||||
tokens.push({ kind: ContentEditableTokenKind.TEXT, text: input.slice(cursor) });
|
||||
tokens.push({ kind: ChatFormInputRichTokenKind.TEXT, text: input.slice(cursor) });
|
||||
}
|
||||
}
|
||||
|
||||
function isCodeBlockElement(node: Node | null): node is HTMLElement {
|
||||
return node instanceof HTMLElement && node.dataset.codeToken === 'block';
|
||||
return (
|
||||
node instanceof HTMLElement && node.dataset.codeToken === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a contenteditable subtree back to source. `<br>` and block
|
||||
* Serialize a chat-form-input-rich subtree back to source. `<br>` and block
|
||||
* wrappers the browser inserted for newlines fold back into `\n` (a
|
||||
* trailing `<br>` is the browser's caret placeholder, not a newline);
|
||||
* any other element is transparent. Code spans serialize their
|
||||
@@ -226,7 +228,7 @@ export function serializeContent(root: HTMLElement): string {
|
||||
}
|
||||
|
||||
if (el.dataset.codeToken !== undefined) {
|
||||
const isBlock = el.dataset.codeToken === 'block';
|
||||
const isBlock = el.dataset.codeToken === ChatFormInputRichTokenKind.CODE_BLOCK;
|
||||
|
||||
if (isBlock && (pendingBlockBoundary || !first)) out += '\n';
|
||||
|
||||
@@ -285,8 +287,8 @@ export function serializeContent(root: HTMLElement): string {
|
||||
* A mismatch means token boundaries shifted (a code span was just
|
||||
* completed or broken) and the DOM needs a rebuild to restyle.
|
||||
*/
|
||||
export function domMatchesTokens(root: HTMLElement, tokens: ContentEditableToken[]): boolean {
|
||||
const expected = tokens.filter((token) => token.kind !== ContentEditableTokenKind.TEXT);
|
||||
export function domMatchesTokens(root: HTMLElement, tokens: ChatFormInputRichToken[]): boolean {
|
||||
const expected = tokens.filter((token) => token.kind !== ChatFormInputRichTokenKind.TEXT);
|
||||
|
||||
let index = 0;
|
||||
|
||||
@@ -309,7 +311,7 @@ export function domMatchesTokens(root: HTMLElement, tokens: ContentEditableToken
|
||||
if (!token) return false;
|
||||
|
||||
if (isBadge) {
|
||||
if (token.kind !== ContentEditableTokenKind.BADGE) return false;
|
||||
if (token.kind !== ChatFormInputRichTokenKind.BADGE) return false;
|
||||
|
||||
if (token.name !== (el.dataset.mentionName ?? '')) return false;
|
||||
|
||||
@@ -318,16 +320,16 @@ export function domMatchesTokens(root: HTMLElement, tokens: ContentEditableToken
|
||||
continue;
|
||||
}
|
||||
|
||||
const codeKind: ContentEditableTokenKind =
|
||||
el.dataset.codeToken === 'block'
|
||||
? ContentEditableTokenKind.CODE_BLOCK
|
||||
: ContentEditableTokenKind.INLINE_CODE;
|
||||
const codeKind: ChatFormInputRichTokenKind =
|
||||
el.dataset.codeToken === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
? ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
: ChatFormInputRichTokenKind.CODE_INLINE;
|
||||
|
||||
if (token.kind !== codeKind) return false;
|
||||
|
||||
if (
|
||||
token.kind === ContentEditableTokenKind.INLINE_CODE ||
|
||||
token.kind === ContentEditableTokenKind.CODE_BLOCK
|
||||
token.kind === ChatFormInputRichTokenKind.CODE_INLINE ||
|
||||
token.kind === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
) {
|
||||
if (token.text !== (el.textContent ?? '')) return false;
|
||||
}
|
||||
@@ -446,7 +448,7 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
|
||||
}
|
||||
|
||||
if (el.dataset.codeToken !== undefined) {
|
||||
const isBlock = el.dataset.codeToken === 'block';
|
||||
const isBlock = el.dataset.codeToken === ChatFormInputRichTokenKind.CODE_BLOCK;
|
||||
|
||||
if (isBlock && !first) {
|
||||
if (!atOrBeforeCaret(el, 0)) {
|
||||
@@ -521,26 +523,29 @@ export function rangeToTextOffset(root: HTMLElement, range: Range | null): numbe
|
||||
* string + inline SVG are shared with the rehype plugin via
|
||||
* `$lib/constants`.
|
||||
*/
|
||||
export function buildFragment(tokens: ContentEditableToken[]): DocumentFragment {
|
||||
export function buildFragment(tokens: ChatFormInputRichToken[]): DocumentFragment {
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
for (let index = 0; index < tokens.length; index++) {
|
||||
const token = tokens[index];
|
||||
|
||||
if (token.kind === ContentEditableTokenKind.TEXT) {
|
||||
if (token.kind === ChatFormInputRichTokenKind.TEXT) {
|
||||
let text = token.text;
|
||||
|
||||
// The separator \n at a fenced-block boundary is synthesized
|
||||
// at serialization time; keeping it in the DOM would render a
|
||||
// phantom empty line next to the block.
|
||||
if (
|
||||
tokens[index - 1]?.kind === ContentEditableTokenKind.CODE_BLOCK &&
|
||||
tokens[index - 1]?.kind === ChatFormInputRichTokenKind.CODE_BLOCK &&
|
||||
text.startsWith('\n')
|
||||
) {
|
||||
text = text.slice(1);
|
||||
}
|
||||
|
||||
if (tokens[index + 1]?.kind === ContentEditableTokenKind.CODE_BLOCK && text.endsWith('\n')) {
|
||||
if (
|
||||
tokens[index + 1]?.kind === ChatFormInputRichTokenKind.CODE_BLOCK &&
|
||||
text.endsWith('\n')
|
||||
) {
|
||||
text = text.slice(0, -1);
|
||||
}
|
||||
|
||||
@@ -552,13 +557,12 @@ export function buildFragment(tokens: ContentEditableToken[]): DocumentFragment
|
||||
}
|
||||
|
||||
if (
|
||||
token.kind === ContentEditableTokenKind.INLINE_CODE ||
|
||||
token.kind === ContentEditableTokenKind.CODE_BLOCK
|
||||
token.kind === ChatFormInputRichTokenKind.CODE_INLINE ||
|
||||
token.kind === ChatFormInputRichTokenKind.CODE_BLOCK
|
||||
) {
|
||||
const code = document.createElement('code');
|
||||
|
||||
code.dataset.codeToken =
|
||||
token.kind === ContentEditableTokenKind.CODE_BLOCK ? 'block' : 'inline';
|
||||
code.dataset.codeToken = token.kind;
|
||||
code.textContent = token.text;
|
||||
fragment.appendChild(code);
|
||||
|
||||
@@ -734,14 +738,14 @@ export function badgeAwareWordJump(
|
||||
|
||||
for (const token of tokenizeContent(source)) {
|
||||
const len =
|
||||
token.kind === ContentEditableTokenKind.BADGE
|
||||
token.kind === ChatFormInputRichTokenKind.BADGE
|
||||
? badgeSourceLength(token.name, token.path)
|
||||
: token.text.length;
|
||||
|
||||
if (token.kind === ContentEditableTokenKind.BADGE)
|
||||
if (token.kind === ChatFormInputRichTokenKind.BADGE)
|
||||
badgeSpans.push([masked.length, masked.length + len]);
|
||||
|
||||
masked += token.kind === ContentEditableTokenKind.BADGE ? 'a'.repeat(len) : token.text;
|
||||
masked += token.kind === ChatFormInputRichTokenKind.BADGE ? 'a'.repeat(len) : token.text;
|
||||
}
|
||||
|
||||
if (badgeSpans.length === 0) return null;
|
||||
@@ -804,7 +808,7 @@ export function badgeAwareWordJump(
|
||||
export function leadingBadgeEdgeOffset(source: string, caret: number): number | null {
|
||||
const [first] = tokenizeContent(source);
|
||||
|
||||
if (!first || first.kind !== ContentEditableTokenKind.BADGE) return null;
|
||||
if (!first || first.kind !== ChatFormInputRichTokenKind.BADGE) return null;
|
||||
|
||||
return caret === badgeSourceLength(first.name, first.path) ? 0 : null;
|
||||
}
|
||||
@@ -912,7 +916,7 @@ export function textOffsetToRange(root: HTMLElement, offset: number): Range {
|
||||
}
|
||||
|
||||
if (el.dataset.codeToken !== undefined) {
|
||||
const isBlock = el.dataset.codeToken === 'block';
|
||||
const isBlock = el.dataset.codeToken === ChatFormInputRichTokenKind.CODE_BLOCK;
|
||||
|
||||
if (isBlock && (pendingBlockBoundary || !first)) {
|
||||
pendingBlockBoundary = false;
|
||||
@@ -221,7 +221,7 @@ export {
|
||||
textOffsetToRange,
|
||||
badgeAwareWordJump,
|
||||
leadingBadgeEdgeOffset
|
||||
} from './contenteditable-tokenizer';
|
||||
} from './chat-form-input-rich-tokenizer';
|
||||
|
||||
// Source-space undo/redo history for the chat-form contenteditable
|
||||
export { SourceHistory, type SourceHistoryEntry } from './source-history';
|
||||
|
||||
Reference in New Issue
Block a user