import re import gradio as gr def replace_first( text: str, find_text: str, replace_text: str, case_sensitive: bool, ) -> tuple[str, str]: text = text or "" if not find_text: return text, "Enter text to find." if case_sensitive: position = text.find(find_text) if position == -1: return text, "Cannot find the specified text." updated_text = ( text[:position] + replace_text + text[position + len(find_text):] ) else: pattern = re.compile(re.escape(find_text), re.IGNORECASE) match = pattern.search(text) if not match: return text, "Cannot find the specified text." updated_text = ( text[:match.start()] + replace_text + text[match.end():] ) return updated_text, "Replaced the first match." def replace_all( text: str, find_text: str, replace_text: str, case_sensitive: bool, ) -> tuple[str, str]: text = text or "" if not find_text: return text, "Enter text to find." if case_sensitive: match_count = text.count(find_text) if match_count == 0: return text, "Cannot find the specified text." updated_text = text.replace(find_text, replace_text) else: pattern = re.compile(re.escape(find_text), re.IGNORECASE) match_count = len(pattern.findall(text)) if match_count == 0: return text, "Cannot find the specified text." updated_text = pattern.sub(lambda _: replace_text, text) word = "match" if match_count == 1 else "matches" return updated_text, f"Replaced {match_count} {word}." def clear_editor() -> tuple[str, str, str, str]: return "", "", "", "Editor cleared." CSS = """ .gradio-container { max-width: none !important; width: 99% !important; margin: 0 auto !important; padding-left: 12px !important; padding-right: 12px !important; } #main-layout { gap: 14px !important; } #editor-column { min-width: 0 !important; } #main-editor textarea { min-height: 760px !important; height: 82vh !important; resize: vertical !important; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace !important; font-size: 16px !important; line-height: 1.55 !important; white-space: pre-wrap !important; tab-size: 4 !important; } #side-panel { min-width: 260px !important; max-width: 290px !important; } #side-panel button { min-height: 42px !important; } #status-box textarea { min-height: 70px !important; } @media (max-width: 900px) { #side-panel { max-width: none !important; } #main-editor textarea { height: 65vh !important; } } """ HEAD = """ """ with gr.Blocks( title="Plain Text Editor", css=CSS, head=HEAD, ) as demo: gr.Markdown("# Plain Text Editor") with gr.Row( equal_height=False, elem_id="main-layout", ): with gr.Column( scale=10, min_width=700, elem_id="editor-column", ): editor = gr.Textbox( label="Text", placeholder="Type or paste your plain text here...", lines=30, max_lines=3000, elem_id="main-editor", buttons=["copy"], autofocus=True, ) with gr.Column( scale=2, min_width=260, elem_id="side-panel", ): paste_button = gr.Button( "Paste", variant="primary", ) clear_button = gr.Button( "Clear Text", variant="stop", ) gr.Markdown("### Find and Replace") find_input = gr.Textbox( label="Find", placeholder="Text to find", lines=1, elem_id="find-input", ) find_next_button = gr.Button( "Find Next (F3)", variant="primary", ) replace_input = gr.Textbox( label="Replace with", placeholder="Replacement text", lines=1, ) case_sensitive = gr.Checkbox( label="Match case", value=False, elem_id="case-sensitive", ) replace_first_button = gr.Button( "Replace First" ) replace_all_button = gr.Button( "Replace All" ) status = gr.Textbox( label="Status", value="Ready.", interactive=False, elem_id="status-box", ) paste_button.click( fn=None, inputs=None, outputs=editor, js=""" async () => { const editor = document.querySelector( "#main-editor textarea" ); if (!editor) { return ""; } try { const clipboardText = await navigator.clipboard.readText(); const currentText = editor.value || ""; const start = editor.selectionStart ?? currentText.length; const end = editor.selectionEnd ?? currentText.length; const updatedText = currentText.slice(0, start) + clipboardText + currentText.slice(end); setTimeout(() => { const updatedEditor = document.querySelector( "#main-editor textarea" ); if (updatedEditor) { const newCursor = start + clipboardText.length; updatedEditor.focus(); updatedEditor.setSelectionRange( newCursor, newCursor ); } }, 100); return updatedText; } catch (error) { alert( "Clipboard access was blocked. " + "Please allow clipboard permission, " + "or use Ctrl+V / Cmd+V." ); return editor.value || ""; } } """, ) find_next_button.click( fn=None, inputs=None, outputs=None, js=""" () => { window.findNext(); } """, ) replace_first_button.click( fn=replace_first, inputs=[ editor, find_input, replace_input, case_sensitive, ], outputs=[ editor, status, ], ) replace_all_button.click( fn=replace_all, inputs=[ editor, find_input, replace_input, case_sensitive, ], outputs=[ editor, status, ], ) clear_button.click( fn=clear_editor, inputs=None, outputs=[ editor, find_input, replace_input, status, ], ) if __name__ == "__main__": demo.launch()