Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Line Breaking</title>
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<div class="line-breaking">
<div class="content">
<h2 class="tool-title">Line Breaking Tool</h2>
<p class="preview display-none"></p>
<button id="insert-text" class="display-none">Insert Text</button>
<div class="file-upload display-none">
<label for="file-input">Upload a .txt or .xml file</label>
<input type="file" id="file-input" accept=".txt,.xml" />
</div>
<button id="upload-file">Upload Text File</button>
<div id="break-options" class="display-none">
<label for="break-tag">Enter line break tag(s) or symbol(s) (comma-separated):</label>
<input type="text" id="break-tag" placeholder="e.g. <lb/>, ¶, --|--" />
</div>
</div>
</div>

<script src="line-breaking.js"></script>
</body>
</html>
117 changes: 117 additions & 0 deletions line-breaking.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
let fileContent = ""
let lines = []

window.addEventListener("load", () => {
const savedData = localStorage.getItem("fileData")
if (savedData) {
const parsed = JSON.parse(savedData)
fileContent = parsed.fileContent || ""
lines = parsed.lines || []
if (fileContent && lines.length > 0) {
renderPreview("", true)
document.querySelector(".preview").classList.remove("display-none")
document.getElementById("insert-text").classList.remove("display-none")
document.getElementById("break-options").classList.remove("display-none")
}
}
})

window.addEventListener("message", async event => {
if (!event?.data) return
if (event.data.type === "SELECT_ANNOTATION" || event.data.type === "CURRENT_LINE_INDEX") {
document.getElementById("insert-text").dataset.lineId = event.data.lineId
}
})

document.getElementById("upload-file").addEventListener("click", () => {
document.querySelector(".file-upload").classList.toggle("display-none")
})

document.getElementById("file-input").addEventListener("change", (event) => {
const file = event.target.files[0]
const preview = document.querySelector(".preview")
preview.innerHTML = ""

if (!file) {
preview.classList.remove("display-none")
preview.innerHTML = "No file selected"
document.getElementById("insert-text").classList.add("display-none")
document.getElementById("break-options").classList.add("display-none")
fileContent = ""
return
}

if (!file.name.endsWith(".txt") && !file.name.endsWith(".xml")) {
preview.innerHTML = "Please upload a valid .txt or .xml file"
document.getElementById("break-options").classList.add("display-none")
fileContent = ""
return
}

preview.classList.remove("display-none")
document.getElementById("insert-text").classList.remove("display-none")

const reader = new FileReader()
reader.onload = function (e) {
fileContent = e.target.result
document.getElementById("break-options").classList.remove("display-none")
renderPreview("")
}
reader.readAsText(file)
document.querySelector(".file-upload").classList.add("display-none")
})

document.getElementById("break-tag").addEventListener("input", () => {
renderPreview(document.getElementById("break-tag").value.trim())
})

function renderPreview(tagInput, fromStorage = false) {
const previewElement = document.querySelector(".preview")

if (!fromStorage) {
if (!fileContent) return
let regexParts = ["\\r?\\n"]
if (tagInput) {
const tags = tagInput.split(",").map(t => t.trim()).filter(Boolean)
tags.forEach(t => regexParts.push(t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")))
}
const regex = new RegExp(regexParts.join("|"), "g")
lines = fileContent.split(regex).map(line => line.trim()).filter(line => line !== "")
}

const formatted = lines.map((line, i) => {
const safeLine = line.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;")
return `<div class="preview-line" data-line="${i}">
<span class="line-number">${i + 1}</span>
<span class="line-content">${safeLine}</span>
</div>`
}).join("")

previewElement.innerHTML = formatted
localStorage.setItem("fileData", JSON.stringify({ fileContent, lines }))

document.querySelectorAll(".preview-line").forEach((div) => {
div.addEventListener("click", () => {
document.querySelectorAll(".preview-line").forEach(d => d.classList.remove("selected"))
div.classList.add("selected")
})
})

document.getElementById("insert-text").addEventListener("click", () => {
const selected = document.querySelector(".preview-line.selected")
if (!selected) return

const lineText = selected.querySelector(".line-content").textContent
const lineIndex = Number(document.getElementById("insert-text").dataset.lineId)
if (isNaN(lineIndex)) return

window.parent?.postMessage({
type: "UPDATE_LINE_TEXT",
lineIndex: lineIndex,
text: lineText
}, "*")

lines.splice(Number(selected.dataset.line), 1)
renderPreview("", true)
})
}
130 changes: 130 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #fef8e4;
margin: 0;
padding: 20px;
color: #333;
}

.line-breaking {
display: flex;
justify-content: center;
align-items: flex-start;
width: 100%;
min-height: 100vh;
box-sizing: border-box;
}

.content {
display: flex;
flex-direction: column;
gap: 15px;
align-items: stretch;
max-width: 900px;
width: 100%;
}

button {
padding: 10px 10px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: 1.5px solid rgb(0, 90, 140);
border-radius: 20px;
background-color: rgb(0, 90, 140);
color: white;
transition: background-color 0.3s ease, border-color 0.3s ease;
margin-bottom: 5px;
}

button:hover {
background-color: white;
color: rgb(0, 90, 140);
}

.file-upload {
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-start;
}

.display-none {
display: none;
}

label {
font-size: 15px;
cursor: pointer;
margin-bottom: 5px;
font-weight: 600;
color: rgb(0, 90, 140);
}

input[type="file"] { cursor: pointer; }

input[type="text"] {
margin-top: 10px;
padding: 6px 8px;
font-size: 15px;
border: 1px solid #ccc;
border-radius: 5px;
width: 100%;
max-width: 300px;
box-sizing: border-box;
}

.tool-title {
margin: 0;
font-size: 24px;
color: rgb(0, 90, 140);
border-bottom: 2px solid rgb(0, 90, 140);
padding-bottom: 5px;
}

.preview {
font-family: monospace;
background: #fff;
border: 1px solid #ccc;
padding: 15px;
white-space: pre-wrap;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
width: 100%;
box-sizing: border-box;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.preview-line {
padding: 2px 5px;
cursor: pointer;
display: flex;
}

.preview-line:hover {
background-color: #e0f0ff;
border: 1px solid rgb(0, 90, 140);
border-radius: 5px;
}

.preview-line.selected {
background-color: #c0e0ff;
border: 2px solid rgb(0, 90, 140);
border-radius: 5px;
}

.line-number {
display: inline-block;
width: 2em;
color: rgb(0, 90, 140);
user-select: none;
font-weight: bold;
}

.line-content {
display: inline-block;
margin-left: 10px;
width: calc(100% - 2em - 10px);
}