From 1d09dbadf3695b959bee22ed2362b6a74d9a0e73 Mon Sep 17 00:00:00 2001 From: anibilag Date: Wed, 26 Feb 2025 23:33:29 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=87=D0=B5?= =?UTF-8?q?=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=D0=BE=D0=B2,=20=D0=B4?= =?UTF-8?q?=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D0=BD=D0=B0=20=D0=BA?= =?UTF-8?q?=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20TipTap=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B6?= =?UTF-8?q?=D0=B8=D1=80=D0=BD=D1=8B=D0=BC.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/components/TipTapEditor.tsx | 242 +++++++++++++++++++++++--------- src/pages/AdminPage.tsx | 65 +++++++-- src/types/index.ts | 2 + 3 files changed, 232 insertions(+), 77 deletions(-) diff --git a/src/components/TipTapEditor.tsx b/src/components/TipTapEditor.tsx index a44855b..081eb3d 100644 --- a/src/components/TipTapEditor.tsx +++ b/src/components/TipTapEditor.tsx @@ -144,6 +144,178 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr editor.chain().focus().updateAttributes('image', {width: `${newWidth}px`}).run(); }; + // Helper function to map paragraph word positions to document positions + const mapWordToDocumentPositions = ( + word: { start: number, end: number }, + textPositions: { start: number, end: number }[], + paragraphOffset: number + ) => { + let docStart = -1; + let docEnd = -1; + + // Find which text node(s) contain this word + for (const pos of textPositions) { + // Word starts in this text node + if (docStart === -1 && word.start < pos.end - paragraphOffset - 1) { + const relativeStart = Math.max(0, word.start - (pos.start - paragraphOffset - 1)); + docStart = pos.start + relativeStart; + } + + // Word ends in this text node + if (docStart !== -1 && word.end <= pos.end - paragraphOffset - 1) { + const relativeEnd = Math.min(pos.end - pos.start, word.end - (pos.start - paragraphOffset - 1)); + docEnd = pos.start + relativeEnd; + break; + } + } + + if (docStart !== -1 && docEnd !== -1) { + return { start: docStart, end: docEnd }; + } + + return null; + }; + + const handleClick = () => { + const { state, dispatch } = editor.view; + const tr = state.tr; + + // Process each paragraph separately to avoid context issues + state.doc.forEach((paragraphNode, paragraphOffset) => { + if (paragraphNode.type.name !== 'paragraph') return; + + // Get the full text of this paragraph + let paragraphText = ''; + const textPositions: { start: number, end: number }[] = []; + + paragraphNode.content.forEach((textNode, textOffset) => { + if (textNode.isText) { + const nodeText = textNode.text || ""; // Handle undefined text + const start = paragraphOffset + 1 + textOffset; // +1 to account for paragraph tag + textPositions.push({ + start, + end: start + nodeText.length + }); + paragraphText += nodeText; + } + }); + + if (!paragraphText) return; + + // Find all valid sentences in the paragraph + const sentenceRegex = /[^.!?]+[.!?]+/g; + const sentences: { text: string, start: number, end: number }[] = []; + let match; + let lastIndex = 0; + + while ((match = sentenceRegex.exec(paragraphText)) !== null) { + sentences.push({ + text: match[0], + start: match.index, + end: sentenceRegex.lastIndex + }); + lastIndex = sentenceRegex.lastIndex; + } + + // Handle text after the last punctuation if it exists + if (lastIndex < paragraphText.length) { + sentences.push({ + text: paragraphText.slice(lastIndex), + start: lastIndex, + end: paragraphText.length + }); + } + + // For each sentence, find capitalized words NOT at the beginning and not after quotes + sentences.forEach(sentence => { + // Split into words, keeping track of positions + const words = []; + const wordRegex = /\S+/g; + let wordMatch; + + while ((wordMatch = wordRegex.exec(sentence.text)) !== null) { + words.push({ + word: wordMatch[0], + start: sentence.start + wordMatch.index, + end: sentence.start + wordRegex.lastIndex + }); + } + + if (words.length === 0) return; + + // Mark the first word as sentence start (shouldn't be bolded if capitalized) + let isFirstWord = true; + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + const currentWord = word.word; + + // Check if the word starts with a capital letter + if (/^[A-ZА-ЯЁ]/u.test(currentWord)) { + // Skip if it's the first word in the sentence + if (isFirstWord) { + isFirstWord = false; + continue; + } + + // Check for various quotation patterns + let afterQuote = false; + + if (i > 0) { + const prevWord = words[i-1].word; + + // Check if previous word ends with quotation characters + // Including standard, curly, angular quotes and special formats like *«* + if (/["""«»''`*]$/.test(prevWord)) { + afterQuote = true; + } else { + // Check for whitespace + quotation mark combination in the text between words + const betweenWords = sentence.text.substring( + words[i-1].end - sentence.start, + word.start - sentence.start + ); + + // Check for standard quotes and special patterns like *«* + if (/\s["""«»''`]/.test(betweenWords) || + /\s\*[«»]\*/.test(betweenWords) || + /\s\*/.test(betweenWords)) { + afterQuote = true; + } + } + } else { + // If it's the first word but after the sentence detection, + // check if the sentence starts with a quotation mark or special pattern + const sentencePrefix = sentence.text.substring(0, word.start - sentence.start).trim(); + if (/^["""«»''`]/.test(sentencePrefix) || + /^\*[«»]\*/.test(sentencePrefix) || + /^\*/.test(sentencePrefix)) { + afterQuote = true; + } + } + + // If not after a quote, bold the word + if (!afterQuote) { + // Map the word position back to the document + const docPositions = mapWordToDocumentPositions(word, textPositions, paragraphOffset); + if (docPositions) { + tr.addMark( + docPositions.start, + docPositions.end, + state.schema.marks.bold.create() + ); + } + } + } + + // Always mark as no longer first word after processing a word + isFirstWord = false; + } + }); + }); + + dispatch(tr); + }; + return (
@@ -159,75 +331,7 @@ export function TipTapEditor({ initialContent, onContentChange }: TipTapEditorPr
@@ -361,8 +389,8 @@ export function AdminPage() {
-

Опубликованные статьи

-
+

Статьи

+
+
    {articles.map((article) => ( -
  • +
  • @@ -410,6 +450,15 @@ export function AdminPage() {

    +