Spaces:
Sleeping
Sleeping
| 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 = """ | |
| <script> | |
| let lastFindText = ""; | |
| let lastCaseSensitive = false; | |
| let lastMatchEnd = 0; | |
| function getEditor() { | |
| return document.querySelector("#main-editor textarea"); | |
| } | |
| function getFindInput() { | |
| return document.querySelector("#find-input textarea"); | |
| } | |
| function getCaseSensitiveCheckbox() { | |
| return document.querySelector( | |
| "#case-sensitive input[type='checkbox']" | |
| ); | |
| } | |
| function setStatus(message) { | |
| const statusBox = document.querySelector("#status-box textarea"); | |
| if (!statusBox) { | |
| return; | |
| } | |
| const nativeSetter = Object.getOwnPropertyDescriptor( | |
| window.HTMLTextAreaElement.prototype, | |
| "value" | |
| ).set; | |
| nativeSetter.call(statusBox, message); | |
| statusBox.dispatchEvent( | |
| new Event("input", { | |
| bubbles: true | |
| }) | |
| ); | |
| } | |
| function resetFindPosition() { | |
| lastFindText = ""; | |
| lastMatchEnd = 0; | |
| } | |
| function findNext() { | |
| const editor = getEditor(); | |
| const findInput = getFindInput(); | |
| const caseCheckbox = getCaseSensitiveCheckbox(); | |
| if (!editor || !findInput) { | |
| return; | |
| } | |
| const originalText = editor.value || ""; | |
| const findText = findInput.value || ""; | |
| const caseSensitive = caseCheckbox | |
| ? caseCheckbox.checked | |
| : false; | |
| if (!findText) { | |
| setStatus("Enter text to find."); | |
| findInput.focus(); | |
| return; | |
| } | |
| const searchText = caseSensitive | |
| ? originalText | |
| : originalText.toLowerCase(); | |
| const searchTerm = caseSensitive | |
| ? findText | |
| : findText.toLowerCase(); | |
| /* | |
| When the search term or case-sensitive option changes, | |
| begin from the editor's current cursor position. | |
| */ | |
| if ( | |
| findText !== lastFindText || | |
| caseSensitive !== lastCaseSensitive | |
| ) { | |
| lastFindText = findText; | |
| lastCaseSensitive = caseSensitive; | |
| lastMatchEnd = editor.selectionEnd || 0; | |
| } else { | |
| /* | |
| Continue after the currently selected match. | |
| */ | |
| lastMatchEnd = editor.selectionEnd || lastMatchEnd; | |
| } | |
| let position = searchText.indexOf( | |
| searchTerm, | |
| lastMatchEnd | |
| ); | |
| let wrapped = false; | |
| /* | |
| Like Notepad, return to the beginning when the end | |
| of the text is reached. | |
| */ | |
| if (position === -1 && lastMatchEnd > 0) { | |
| position = searchText.indexOf(searchTerm, 0); | |
| wrapped = position !== -1; | |
| } | |
| if (position === -1) { | |
| setStatus( | |
| 'Cannot find "' + findText + '".' | |
| ); | |
| return; | |
| } | |
| const matchEnd = position + findText.length; | |
| editor.focus(); | |
| editor.setSelectionRange(position, matchEnd); | |
| lastMatchEnd = matchEnd; | |
| /* | |
| Scroll the selected match into view. A temporary hidden | |
| mirror element is used to estimate the vertical position. | |
| */ | |
| scrollSelectionIntoView( | |
| editor, | |
| position, | |
| matchEnd | |
| ); | |
| if (wrapped) { | |
| setStatus( | |
| "Reached the end. Continued from the beginning." | |
| ); | |
| } else { | |
| setStatus( | |
| "Match found at character " + (position + 1) + "." | |
| ); | |
| } | |
| } | |
| function scrollSelectionIntoView( | |
| textarea, | |
| selectionStart, | |
| selectionEnd | |
| ) { | |
| const mirror = document.createElement("div"); | |
| const style = window.getComputedStyle(textarea); | |
| const copiedProperties = [ | |
| "boxSizing", | |
| "width", | |
| "height", | |
| "overflowX", | |
| "overflowY", | |
| "borderTopWidth", | |
| "borderRightWidth", | |
| "borderBottomWidth", | |
| "borderLeftWidth", | |
| "paddingTop", | |
| "paddingRight", | |
| "paddingBottom", | |
| "paddingLeft", | |
| "fontStyle", | |
| "fontVariant", | |
| "fontWeight", | |
| "fontStretch", | |
| "fontSize", | |
| "fontSizeAdjust", | |
| "lineHeight", | |
| "fontFamily", | |
| "textAlign", | |
| "textTransform", | |
| "textIndent", | |
| "textDecoration", | |
| "letterSpacing", | |
| "wordSpacing", | |
| "tabSize" | |
| ]; | |
| copiedProperties.forEach((property) => { | |
| mirror.style[property] = style[property]; | |
| }); | |
| mirror.style.position = "absolute"; | |
| mirror.style.visibility = "hidden"; | |
| mirror.style.whiteSpace = "pre-wrap"; | |
| mirror.style.wordWrap = "break-word"; | |
| mirror.style.top = "0"; | |
| mirror.style.left = "-9999px"; | |
| mirror.style.height = "auto"; | |
| const textBeforeMatch = textarea.value.substring( | |
| 0, | |
| selectionStart | |
| ); | |
| mirror.textContent = textBeforeMatch; | |
| const marker = document.createElement("span"); | |
| marker.textContent = | |
| textarea.value.substring( | |
| selectionStart, | |
| selectionEnd | |
| ) || "."; | |
| mirror.appendChild(marker); | |
| document.body.appendChild(mirror); | |
| const targetTop = marker.offsetTop; | |
| const visibleTop = textarea.scrollTop; | |
| const visibleBottom = | |
| textarea.scrollTop + textarea.clientHeight; | |
| if ( | |
| targetTop < visibleTop || | |
| targetTop > visibleBottom - 40 | |
| ) { | |
| textarea.scrollTop = Math.max( | |
| 0, | |
| targetTop - textarea.clientHeight / 2 | |
| ); | |
| } | |
| document.body.removeChild(mirror); | |
| } | |
| /* | |
| Keyboard shortcuts: | |
| F3 Find next | |
| Enter Find next while Find box is focused | |
| Ctrl/Cmd + F Focus the Find box | |
| Escape Return focus to the editor | |
| */ | |
| document.addEventListener("keydown", function(event) { | |
| const findInput = getFindInput(); | |
| const editor = getEditor(); | |
| if (event.key === "F3") { | |
| event.preventDefault(); | |
| findNext(); | |
| return; | |
| } | |
| if ( | |
| (event.ctrlKey || event.metaKey) && | |
| event.key.toLowerCase() === "f" | |
| ) { | |
| event.preventDefault(); | |
| if (findInput) { | |
| findInput.focus(); | |
| findInput.select(); | |
| } | |
| return; | |
| } | |
| if ( | |
| event.key === "Enter" && | |
| document.activeElement === findInput | |
| ) { | |
| event.preventDefault(); | |
| findNext(); | |
| return; | |
| } | |
| if (event.key === "Escape" && editor) { | |
| editor.focus(); | |
| } | |
| }); | |
| /* | |
| Reset search continuation when the search text changes. | |
| */ | |
| document.addEventListener("input", function(event) { | |
| if ( | |
| event.target === getFindInput() || | |
| event.target === getEditor() | |
| ) { | |
| resetFindPosition(); | |
| } | |
| }); | |
| /* | |
| Expose the function so the Gradio button can call it. | |
| */ | |
| window.findNext = findNext; | |
| </script> | |
| """ | |
| 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() |