Skip to content

fix/enhanced-smart-editor - #158

Open
NethmikaKekuu wants to merge 22 commits into
LDFLK:mainfrom
NethmikaKekuu:fix/enhanced-smart-editor
Open

NethmikaKekuu wants to merge 22 commits into
LDFLK:mainfrom
NethmikaKekuu:fix/enhanced-smart-editor

Conversation

@NethmikaKekuu

Copy link
Copy Markdown
Member

Enhancing the Text Editor UI

Fixes #154

Description

Resolved multiple issues with content quality when pasting from Google Docs into the SmartEditor, fixed inline formatting rendering in the PDF, added a live page boundary indicator in the editor so users can see where PDF pages will break while editing, and fixed two regressions in the variable/template workflow that surfaced while testing the paste-cleanup changes.

Changes

SmartEditor.tsx

  • Added cleanPastedHtml() to strip Google Docs wrapper tags (docs-internal-guid) that caused entire documents to appear bold after paste
  • Added conversion of Google Docs inline span styles (font-weight:700, font-style:italic, text-decoration:underline) to semantic HTML tags on paste
  • Fixed wrap() in htmlToMarkdown() to drop bold/italic markers when they wrap multi-line content, preventing literal ** stars appearing at the start and end of pasted documents
  • Rewrote cleanPastedHtml() to operate on a parsed DOM tree instead of regex over the raw HTML string. The previous regex-based span stripping matched on the first closing </span> it found, which incorrectly unwrapped the inner label/remove-button spans of a variable pill instead of the pill's own outer wrapper, silently destroying the data-code attribute and turning the pill into plain text (e.g. {{receiver_position}} became Receiver Position×). Copying or pasting any paragraph containing a pill — including duplicating content within or between templates — permanently broke that variable. The new version walks the DOM, leaves anything inside [data-code] untouched, and only converts or unwraps ordinary spans
  • Added PageBreak custom TipTap node with visible dashed line rendering in the editor and <!--PAGE_BREAK--> serialisation to markdown
  • Added Page Break toolbar button to insert manual page breaks at cursor position
  • Added updatePageLines() which injects dashed visual boundary lines into the scroll container div (not ProseMirror) at calculated PDF page boundaries. Page height in pixels is derived at runtime from the editor's actual rendered offsetWidth divided by the known PDF content width (172mm = PAGE_W(210) - MARGIN(19)*2), multiplied by the usable PDF page height (232mm = BOTTOM_LIMIT(267) - CONTENT_START_Y(35)). This avoids hardcoding a DPI assumption and gives accurate placement on any screen resolution
  • Replaced fixed overlay div approach with injection into the scroll container so boundary lines scroll with content rather than remaining fixed relative to the viewport
  • Added window resize listener to updatePageLines() so boundary lines reposition correctly when the editor panel is resized
  • Page boundary lines carry a pdf-page-line class and are excluded from htmlToMarkdown() serialisation so they never appear in saved content or the PDF
  • Page boundary lines update on every editor change, paste, format action, variable insert, and window resize

Templates.tsx

  • Fixed saveTemplate() blanking the editor immediately after a successful save. The success handler only fell back to the locally-generated markdown when the API response's content field was exactly undefined; if the backend instead returned content: null, or omitted/reshaped the field, that check passed through and selectedTemplate.content became null. That null then propagated into SmartEditor as the initialMarkdown prop, whose sync effect treats falsy markdown as empty and force-reset the live editor to blank right after the save completed — for both newly-created and edited templates. The handler now always overlays the known-good markdown onto the saved template instead of trusting whatever shape the API echoes back

pdfUtils.ts

  • Added <!--PAGE_BREAK--> detection in the main render loop to call doc.addPage() and reset cursorY
  • Added justify alignment support throughout — regex patterns, type definitions, and getAlignX() now accept justify as a valid alignment value
  • Added renderRowJustified() for proper full-justification rendering — spaces between words are expanded so each non-last line fills the full content width
  • Fixed italic regex match that was previously setting underline: false instead of italic: true on match[3]
  • Added sanitizeForPDF() applied at three levels — on the full markdown string after variable replacement, inside parseInlineSegments() before parsing, and per-token inside renderRow() before doc.text() — to prevent jsPDF latin1 encoding errors from corrupting character spacing
  • Fixed segmentsToTokens() to split on unicode whitespace (\u00A0) in addition to regular spaces, preventing first/last name merging from database values that use non-breaking spaces

Related Issues

  • Fixes Google Docs paste losing bold, italic, underline formatting
  • Fixes entire document appearing bold after paste (Google Docs guid wrapper)
  • Fixes literal ** stars appearing at start and end of pasted content
  • Fixes text alignment (center, right, justify) not applying in PDF
  • Fixes letter spacing corruption on variable-replaced text from database
  • Fixes variable name merging (e.g. BimalSilva instead of Bimal Silva)
  • Fixes page boundary lines not scrolling with content (were fixed to viewport)
  • Fixes page boundary lines appearing at wrong positions due to hardcoded 96dpi assumption
  • Fixes variable placeholders being permanently corrupted into plain text when copy-pasting content that contains an inserted variable
  • Fixes templates appearing blank immediately after saving, for both newly-created and existing templates

Testing

  • Paste bold, italic, underlined, and heading-formatted content from Google Docs and verify formatting is preserved in both the editor and the downloaded PDF
  • Paste a fully bolded Google Docs document and confirm no ** stars appear at the beginning or end
  • Apply center, right, and justify alignment to paragraphs and verify correct positioning in the PDF
  • Create a sender with a multi-word name and verify it renders with correct spacing in the PDF
  • Insert a Page Break and verify the next line starts on a new page in the PDF
  • Type enough content to exceed one page and verify the dashed page boundary line appears in the editor at the correct position and scrolls with the content
  • Resize the editor panel and verify page boundary lines reposition correctly
  • Verify page boundary lines do not appear in the downloaded PDF
  • Verify page boundary lines update position as content is added or removed
  • Insert a variable, copy a sentence containing it, paste it elsewhere in the same or a different template, and confirm the pasted variable is still a live pill and still substitutes correctly when generating an RTI
  • Create a new template and save it, then edit an existing template and save it; confirm the editor still shows the saved content immediately after each save instead of going blank

@CLAassistant

CLAassistant commented Jun 17, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request replaces the custom contenteditable editor with a Tiptap-based implementation in SmartEditor.tsx and rewrites the PDF generation logic in pdfUtils.ts to achieve layout parity. However, several critical issues were identified in the review: cleanPastedHtml still relies on regex and incorrectly strips variable pill spans; asynchronous image loading in pdfUtils.ts leads to a missing logo in the generated PDF; using indexOf inside the line-processing loop introduces correctness bugs and O(N^2) performance degradation; and synchronous layout calculations on every keystroke in onUpdate cause layout thrashing and typing lag.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +374 to +386
function cleanPastedHtml(raw: string): string {
return raw
.replace(/<b\s+id="docs-internal-guid[^"]*"[^>]*>([\s\S]*?)<\/b>/gi, '$1')
.replace(/<b\s+style="[^"]*font-weight:\s*normal[^"]*"[^>]*>([\s\S]*?)<\/b>/gi, '$1')
.replace(/<meta[^>]*>/gi, '')
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
.replace(/\s*id="docs-internal-guid[^"]*"/gi, '')
.replace(/<span([^>]*)font-weight\s*:\s*(?:bold|700|800|600)([^>]*)>([\s\S]*?)<\/span>/gi, '<strong>$3</strong>')
.replace(/<span([^>]*)font-style\s*:\s*italic([^>]*)>([\s\S]*?)<\/span>/gi, '<em>$3</em>')
.replace(/<span([^>]*)text-decoration\s*:[^;]*underline([^>]*)>([\s\S]*?)<\/span>/gi, '<u>$3</u>')
.replace(/<span[^>]*>([\s\S]*?)<\/span>/gi, '$1')
.replace(/[ \t]{2,}/g, ' ');
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The PR description states that cleanPastedHtml() was rewritten to operate on a parsed DOM tree instead of regex to prevent destroying the data-code (or data-variable) attributes on variable pills. However, the implemented code is still entirely regex-based and contains .replace(/\u003cspan[^\u003e]*\u003e([\s\S]*?)\u003c\/span\u003e/gi, '$1'), which will strip all spans (including variable pills) and convert them to plain text. Please implement the DOM-parsing approach as described to prevent this regression.

Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
@NethmikaKekuu NethmikaKekuu reopened this Jun 18, 2026
@NethmikaKekuu

Copy link
Copy Markdown
Member Author

/gemini reveiw

@gemini-code-assist

Copy link
Copy Markdown
Contributor

It looks like you're trying to trigger a code review with /gemini reveiw. Please note that the correct command is /gemini review (with the correct spelling). Once you use the correct command, I will be happy to perform the review for you.

@NethmikaKekuu

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the SmartEditor component to use the Tiptap editor framework instead of a custom contenteditable implementation, adding custom nodes for variable pills and page breaks, and updates the PDF generation utility (pdfUtils.ts) to align with the new editor layout. Feedback on the changes highlights a bug in pdfUtils.ts where loading the logo image synchronously inside the page loop fails in browser environments, recommending pre-loading it asynchronously as a base64 data URL instead. Additionally, the reviewer suggests replacing global window custom events in SmartEditor.tsx with a ref-based approach to avoid conflicts between multiple editor instances, along with minor cleanups such as removing unused event listeners and an empty JSX expression container.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
@NethmikaKekuu

Copy link
Copy Markdown
Member Author

/gemini review

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the rich text editing and PDF generation system by replacing the basic contenteditable editor with a Tiptap-based SmartEditor and aligning the jsPDF rendering logic in pdfUtils.ts to match the editor's layout and page-break preview. Key feedback includes addressing a missing dependency in the editor's markdown syncing useEffect, resolving a character sanitization issue in pdfUtils.ts that strips non-Latin-1 characters (preventing Sinhala/Tamil rendering), improving type safety by replacing any with NodeViewProps, eliminating duplicated layout constants, refactoring indirect window event listeners for paste/drop actions into direct editor commands, and avoiding hardcoded configuration values in the footer.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
Comment on lines +563 to +585
handlePaste(_view, event) {
event.preventDefault();
const cd = event.clipboardData;
if (!cd) return false;
const htmlData = cd.getData('text/html');
if (htmlData) {
const md = htmlToMarkdown(cleanPastedHtml(htmlData), placeholdersRef.current);
window.dispatchEvent(new CustomEvent('smarteditor:insert', {
detail: { html: markdownToHtml(md, placeholdersRef.current) }
}));
return true;
}
const textData = cd.getData('text/plain');
if (textData) {
const finalHtml = textData
.split(/\r?\n/)
.map(l => l.trim() ? `<p>${applyInlineMarkdown(l)}</p>` : '<p></p>')
.join('');
window.dispatchEvent(new CustomEvent('smarteditor:insert', { detail: { html: finalHtml } }));
return true;
}
return false;
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The handlePaste logic uses a custom window event (smarteditor:insert) to insert content, which is then handled by a useEffect listener. This approach is indirect and not idiomatic for a React component. Since the editor instance is available in this scope, you can call its commands directly for a simpler and more direct implementation. This would also remove the need for the corresponding useEffect listeners.

        handlePaste(_view, event) {
          if (!editor) return false;
          event.preventDefault();
          const cd = event.clipboardData;
          if (!cd) return false;

          let htmlToInsert = '';
          const htmlData = cd.getData('text/html');
          if (htmlData) {
            const md = htmlToMarkdown(cleanPastedHtml(htmlData), placeholdersRef.current);
            htmlToInsert = markdownToHtml(md, placeholdersRef.current);
          } else {
            const textData = cd.getData('text/plain');
            if (textData) {
              htmlToInsert = textData
                .split(/\r?\n/)
                .map(l => l.trim() ? `<p>${applyInlineMarkdown(l)}</p>` : '<p></p>')
                .join('');
            }
          }

          if (htmlToInsert) {
            editor.commands.insertContent(htmlToInsert, { parseOptions: { preserveWhitespace: 'full' } });
            return true;
          }

          return false;
        },

Comment thread tool/frontend/src/utils/pdfUtils.ts Resolved
…directly and updates the useEffect dependency array.
@NethmikaKekuu
NethmikaKekuu requested a review from ChanukaUOJ June 19, 2026 03:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[RTI][TOOL][FRONTEND] Enhance the smart editor

2 participants