textbox / app.py
leadingbridge's picture
Update app.py
ba66fd7 verified
Raw
History Blame Contribute Delete
14.2 kB
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()