aboutsummaryrefslogtreecommitdiff
path: root/webAO
diff options
context:
space:
mode:
authorOsmium Sorcerer <os@sof.beauty>2026-06-03 11:08:07 +0000
committerOsmium Sorcerer <os@sof.beauty>2026-06-06 03:09:27 +0000
commitbd8b53cd6046cef9802d593d8257392d81afb5ce (patch)
treede52981395cb330f65232f0edbd48dbd4fdf9739 /webAO
parentda790cb4fa1cc221a0b557d2cf64585b8e4ebcf9 (diff)
Eliminate innerHTML manipulation
Construct DOM nodes directly instead of trying to sanitize every input string and dynamically updating HTML. Replace all uses of innerHTML with textContent, replaceChildren, and appendChild. This removes the need to use safeTags and replace newlines, but now requires preserving whitespace via CSS pre-wrap. Every OOC chat line is now placed into its own element instead of simply being appended to the log. This might be worse, and createTextNode is another alternative.
Diffstat (limited to 'webAO')
-rw-r--r--webAO/client.ts4
-rw-r--r--webAO/client/createArea.ts2
-rw-r--r--webAO/client/fetchLists.ts6
-rw-r--r--webAO/client/handleBans.ts6
-rw-r--r--webAO/client/saveChatLogHandle.ts4
-rw-r--r--webAO/dom/changeCharacter.ts2
-rw-r--r--webAO/dom/musicListFilter.ts2
-rw-r--r--webAO/dom/renderPlayerList.ts2
-rw-r--r--webAO/master.ts36
-rw-r--r--webAO/packets/handlers/handleARUP.ts2
-rw-r--r--webAO/packets/handlers/handleCT.ts11
-rw-r--r--webAO/packets/handlers/handleLE.ts2
-rw-r--r--webAO/packets/handlers/handlePV.ts12
-rw-r--r--webAO/packets/handlers/handleSI.ts2
-rw-r--r--webAO/packets/handlers/handleSM.ts4
-rw-r--r--webAO/packets/handlers/handleZZ.ts8
-rw-r--r--webAO/styles/client.css8
-rw-r--r--webAO/styles/master.css4
-rw-r--r--webAO/ui.js2
-rw-r--r--webAO/utils/aoml.ts6
-rw-r--r--webAO/viewport/utils/handleICSpeaking.ts8
-rw-r--r--webAO/viewport/viewport.ts16
22 files changed, 92 insertions, 57 deletions
diff --git a/webAO/client.ts b/webAO/client.ts
index 43b04f0c..f013f0a4 100644
--- a/webAO/client.ts
+++ b/webAO/client.ts
@@ -415,12 +415,12 @@ class Client extends EventEmitter {
resetMusicList() {
this.musics = [];
- document.getElementById("client_musiclist").innerHTML = "";
+ document.getElementById("client_musiclist").replaceChildren();
}
resetAreaList() {
this.areas = [];
- document.getElementById("areas").innerHTML = "";
+ document.getElementById("areas").replaceChildren();
fetchBackgroundList();
fetchEvidenceList();
fetchCharacterList();
diff --git a/webAO/client/createArea.ts b/webAO/client/createArea.ts
index 9a40bef4..a90b49ad 100644
--- a/webAO/client/createArea.ts
+++ b/webAO/client/createArea.ts
@@ -18,7 +18,7 @@ export const createArea = (id: number, aname: string) => {
const newarea = document.createElement("SPAN");
newarea.className = "area-button area-default";
newarea.id = `area${id}`;
- newarea.innerText = thisarea.name;
+ newarea.textContent = thisarea.name;
newarea.title =
`Players: ${thisarea.players}\n` +
`Status: ${thisarea.status}\n` +
diff --git a/webAO/client/fetchLists.ts b/webAO/client/fetchLists.ts
index 4e32abc9..710bc0e1 100644
--- a/webAO/client/fetchLists.ts
+++ b/webAO/client/fetchLists.ts
@@ -9,7 +9,7 @@ export const fetchBackgroundList = async () => {
// the try catch will fail before here when there is no file
const bg_select = <HTMLSelectElement>document.getElementById("bg_select");
- bg_select.innerHTML = "";
+ bg_select.replaceChildren();
bg_select.add(new Option("Custom", "0"));
bg_array.forEach((background: string) => {
@@ -24,7 +24,7 @@ export const fetchCharacterList = async () => {
const char_select = <HTMLSelectElement>(
document.getElementById("client_iniselect")
);
- char_select.innerHTML = "";
+ char_select.replaceChildren();
char_select.add(new Option("Custom", "0"));
@@ -43,7 +43,7 @@ export const fetchCharacterList = async () => {
export const fetchEvidenceList = async () => {
const evi_select = <HTMLSelectElement>document.getElementById("evi_select");
- evi_select.innerHTML = "";
+ evi_select.replaceChildren();
evi_select.add(new Option("Custom", "0"));
diff --git a/webAO/client/handleBans.ts b/webAO/client/handleBans.ts
index 004b27ee..4abd1f07 100644
--- a/webAO/client/handleBans.ts
+++ b/webAO/client/handleBans.ts
@@ -7,8 +7,6 @@ import { safeTags } from "../encoding";
*/
export const handleBans = (type: string, reason: string) => {
document.getElementById("client_error_overlay")!.style.display = "flex";
- document.getElementById("client_errortext")!.innerHTML =
- `${type}:<br>${safeTags(reason).replace(/\n/g, "<br />")}`;
- (<HTMLElement>document.getElementById("client_reconnect")).style.display =
- "none";
+ document.getElementById("client_errortext")!.textContent = `${type}:\n${reason}`;
+ document.getElementById("client_reconnect")!.style.display = "none";
};
diff --git a/webAO/client/saveChatLogHandle.ts b/webAO/client/saveChatLogHandle.ts
index 5e6b277b..b184e09d 100644
--- a/webAO/client/saveChatLogHandle.ts
+++ b/webAO/client/saveChatLogHandle.ts
@@ -8,8 +8,8 @@ export const saveChatlogHandle = async () => {
for (let i = 0; i < icMessageLogs.length; i++) {
const SHOWNAME_POSITION = 0;
const TEXT_POSITION = 2;
- const showname = icMessageLogs[i].children[SHOWNAME_POSITION].innerHTML;
- const text = icMessageLogs[i].children[TEXT_POSITION].innerHTML;
+ const showname = icMessageLogs[i].children[SHOWNAME_POSITION].textContent;
+ const text = icMessageLogs[i].children[TEXT_POSITION].textContent;
const message = `${showname}: ${text}`;
messages.push(message);
}
diff --git a/webAO/dom/changeCharacter.ts b/webAO/dom/changeCharacter.ts
index abf6f71e..586b1446 100644
--- a/webAO/dom/changeCharacter.ts
+++ b/webAO/dom/changeCharacter.ts
@@ -5,6 +5,6 @@
export function changeCharacter(_event: Event) {
document.getElementById("client_waiting")!.style.display = "block";
document.getElementById("client_charselect")!.style.display = "block";
- document.getElementById("client_emo")!.innerHTML = "";
+ document.getElementById("client_emo")!.replaceChildren();
}
window.changeCharacter = changeCharacter;
diff --git a/webAO/dom/musicListFilter.ts b/webAO/dom/musicListFilter.ts
index b0a8c3ba..ce6b2f67 100644
--- a/webAO/dom/musicListFilter.ts
+++ b/webAO/dom/musicListFilter.ts
@@ -11,7 +11,7 @@ export function musiclist_filter(_event: Event) {
document.getElementById("client_musicsearch")
)).value;
- musiclist_element.innerHTML = "";
+ musiclist_element.replaceChildren();
for (const trackname of client.musics) {
if (trackname.toLowerCase().indexOf(searchname.toLowerCase()) !== -1) {
diff --git a/webAO/dom/renderPlayerList.ts b/webAO/dom/renderPlayerList.ts
index c238f28b..6c696acc 100644
--- a/webAO/dom/renderPlayerList.ts
+++ b/webAO/dom/renderPlayerList.ts
@@ -3,7 +3,7 @@ import { AO_HOST } from "../client/aoHost";
export function renderPlayerList() {
const list = document.getElementById("client_playerlist") as HTMLTableElement;
- list.innerHTML = "";
+ list.replaceChildren();
const header = list.createTHead().insertRow();
for (const label of ["Icon", "Character", "Showname", "OOC Name"]) {
diff --git a/webAO/master.ts b/webAO/master.ts
index b40763de..569795e9 100644
--- a/webAO/master.ts
+++ b/webAO/master.ts
@@ -167,13 +167,31 @@ function addServer(server: AOServer) {
servers.push(server);
- document.getElementById("masterlist").innerHTML +=
- `<details name="servers">` +
- `<summary><p>${safeTags(server.name)} (${server.players})</p>` +
- `<a class="button" href="${fullClientJoinURL}" target="_blank">Join</a>` +
- `<a class="button" href="${fullClientWatchURL}" target="_blank">Watch</a></summary>` +
- `<p>${safeTags(server.description)}</p>` +
- `</details>`;
+ const masterlist = document.getElementById("masterlist");
+ const details = document.createElement("details");
+ details.name = "servers";
+ const summary = document.createElement("summary");
+ const title = document.createElement("p");
+ title.textContent = `${server.name} (${server.players})`;
+
+ const join = document.createElement("a");
+ join.className = "button";
+ join.href = fullClientJoinURL;
+ join.target = "_blank";
+ join.textContent = "Join";
+
+ const watch = document.createElement("a");
+ watch.className = "button";
+ watch.href = fullClientWatchURL;
+ watch.target = "_blank";
+ watch.textContent = "Watch";
+
+ summary.append(title, join, watch);
+ const desc = document.createElement("p");
+ desc.className = "server-description";
+ desc.textContent = server.description;
+ details.append(summary, desc);
+ masterlist.appendChild(details);
}
function processServerlist(serverlist: AOServer[]) {
@@ -196,10 +214,10 @@ async function getMasterVersion(): Promise<string> {
}
function processClientVersion(data: string) {
- document.getElementById("clientinfo").innerHTML = `Client version: ${data}`;
+ document.getElementById("clientinfo").textContent = `Client version: ${data}`;
}
function processMasterVersion(data: string) {
- document.getElementById("serverinfo").innerHTML =
+ document.getElementById("serverinfo").textContent =
`Master server version: ${data}`;
}
diff --git a/webAO/packets/handlers/handleARUP.ts b/webAO/packets/handlers/handleARUP.ts
index 7f72b156..7f1c701d 100644
--- a/webAO/packets/handlers/handleARUP.ts
+++ b/webAO/packets/handlers/handleARUP.ts
@@ -30,7 +30,7 @@ export const handleARUP = (args: string[]) => {
i
].status.toLowerCase()}`;
- thisarea.innerText = `${client.areas[i].name} (${client.areas[i].players}) [${client.areas[i].status}]`;
+ thisarea.textContent = `${client.areas[i].name} (${client.areas[i].players}) [${client.areas[i].status}]`;
thisarea.title =
`Players: ${client.areas[i].players}\n` +
diff --git a/webAO/packets/handlers/handleCT.ts b/webAO/packets/handlers/handleCT.ts
index 29ec94c0..d4868bd4 100644
--- a/webAO/packets/handlers/handleCT.ts
+++ b/webAO/packets/handlers/handleCT.ts
@@ -9,12 +9,13 @@ const { mode } = queryParser();
export const handleCT = (args: string[]) => {
if (mode !== "replay") {
const oocLog = document.getElementById("client_ooclog")!;
- const username = prepChat(args[1]);
- let message = addLinks(prepChat(args[2]));
- // Replace newlines with br
- message = message.replace(/\n/g, "<br>");
+ const username = unescapeChat(args[1]);
+ let message = addLinks(unescapeChat(args[2]));
- oocLog.innerHTML += `${username}: ${message}<br>`;
+ const line = document.createElement("div");
+ line.className = "ooc-line";
+ line.textContent = `${username}: ${message}`;
+ oocLog.appendChild(line);
if (oocLog.scrollTop + oocLog.offsetHeight + 120 > oocLog.scrollHeight)
oocLog.scrollTo(0, oocLog.scrollHeight);
}
diff --git a/webAO/packets/handlers/handleLE.ts b/webAO/packets/handlers/handleLE.ts
index ae1bb02c..dd2a2f13 100644
--- a/webAO/packets/handlers/handleLE.ts
+++ b/webAO/packets/handlers/handleLE.ts
@@ -22,7 +22,7 @@ export const handleLE = (args: string[]) => {
}
const evidence_box = document.getElementById("evidences");
- evidence_box.innerHTML = "";
+ evidence_box.replaceChildren();
for (let i = 0; i <= client.evidences.length - 1; i++) {
const evi_item = new Image();
evi_item.id = "evi_" + i;
diff --git a/webAO/packets/handlers/handlePV.ts b/webAO/packets/handlers/handlePV.ts
index ba48f36f..2e14ad2f 100644
--- a/webAO/packets/handlers/handlePV.ts
+++ b/webAO/packets/handlers/handlePV.ts
@@ -34,15 +34,17 @@ export const handlePV = async (args: string[]) => {
const { emotes } = client;
const emotesList = document.getElementById("client_emo");
emotesList.style.display = "";
- emotesList.innerHTML = ""; // Clear emote box
+ emotesList.replaceChildren();
const ini = await ensureCharIni(client.charID);
me.side = ini.options.side;
updateActionCommands(me.side);
if (ini.emotions.number === 0) {
- emotesList.innerHTML = `<span
- id="emo_0"
- alt="unavailable"
- class="emote_button">No emotes available</span>`;
+ let emotesUnavail = document.createElement("span");
+ emotesUnavail.id = "emo_0";
+ emotesUnavail.className = "emote_button";
+ emotesUnavail.textContent = "No emotes available";
+
+ emotesList.replaceChildren(emotesUnavail);
} else {
// Probe extensions once using button1_off, then reuse for all emotes
const charPath = `${AO_HOST}characters/${encodeURI(me.name)}/emotions/`;
diff --git a/webAO/packets/handlers/handleSI.ts b/webAO/packets/handlers/handleSI.ts
index eac84e03..369806ba 100644
--- a/webAO/packets/handlers/handleSI.ts
+++ b/webAO/packets/handlers/handleSI.ts
@@ -14,7 +14,7 @@ export const handleSI = (args: string[]) => {
fetchExtensions();
// create the charselect grid, to be filled by the character loader
- document.getElementById("client_chartable")!.innerHTML = "";
+ document.getElementById("client_chartable")!.replaceChildren();
for (let i = 0; i < client.char_list_length; i++) {
const demothing = document.createElement("img");
diff --git a/webAO/packets/handlers/handleSM.ts b/webAO/packets/handlers/handleSM.ts
index 1c3fd5e2..21f6ca59 100644
--- a/webAO/packets/handlers/handleSM.ts
+++ b/webAO/packets/handlers/handleSM.ts
@@ -8,13 +8,13 @@ import { createArea } from "../../client/createArea";
* @param {Array} args packet arguments
*/
export const handleSM = (args: string[]) => {
- document.getElementById("client_loadingtext")!.innerHTML = "Loading Music ";
+ document.getElementById("client_loadingtext")!.textContent = "Loading Music ";
client.resetMusicList();
client.resetAreaList();
client.musics_time = false;
- document.getElementById("client_loadingtext")!.innerHTML = `Loading Music`;
+ document.getElementById("client_loadingtext")!.textContent = `Loading Music`;
for (let i = 1; i < args.length - 1; i++) {
// Check when found the song for the first time
diff --git a/webAO/packets/handlers/handleZZ.ts b/webAO/packets/handlers/handleZZ.ts
index 0496d420..66e7d15a 100644
--- a/webAO/packets/handlers/handleZZ.ts
+++ b/webAO/packets/handlers/handleZZ.ts
@@ -8,8 +8,12 @@ import { prepChat } from "../../encoding";
*/
export const handleZZ = (args: string[]) => {
const oocLog = document.getElementById("client_ooclog")!;
- const message = args[1].replace(/\n/g, "<br>");
- oocLog.innerHTML += `$Alert: ${prepChat(message)}<br>`;
+
+ const modAlert = document.createElement("div");
+ modAlert.textContent = `$Alert: ${unescapeChat(args[1])}`;
+ modAlert.className = "ooc-line";
+ oocLog.appendChild(modAlert);
+
if (oocLog.scrollTop > oocLog.scrollHeight - 60) {
oocLog.scrollTop = oocLog.scrollHeight;
}
diff --git a/webAO/styles/client.css b/webAO/styles/client.css
index 1c1b5c20..fa01ddc6 100644
--- a/webAO/styles/client.css
+++ b/webAO/styles/client.css
@@ -809,3 +809,11 @@
left: 0.5em;
margin-right: -50%;
}
+
+.ooc-line {
+ white-space: pre-wrap;
+}
+
+#client_errortext {
+ white-space: pre-wrap;
+}
diff --git a/webAO/styles/master.css b/webAO/styles/master.css
index cf9a7035..add9ed21 100644
--- a/webAO/styles/master.css
+++ b/webAO/styles/master.css
@@ -375,3 +375,7 @@ ul.navbar-nav.ml-auto {
nav.navbar.fixed-top.navbar-expand-lg.navbar-dark.bg-dark.fixed-top {
height: 65px;
}
+
+.server-description {
+ white-space: pre-wrap;
+}
diff --git a/webAO/ui.js b/webAO/ui.js
index c3a41585..1afd4615 100644
--- a/webAO/ui.js
+++ b/webAO/ui.js
@@ -194,7 +194,7 @@ golden.registerComponentFactoryFunction(
"template",
(container, componentState) => {
const template = document.querySelector(`#${componentState.id}`);
- container.element.innerHTML = template.innerHTML;
+ container.element.replaceChildren(template.content.cloneNode(true));
},
);
if (isMobileDevice) {
diff --git a/webAO/utils/aoml.ts b/webAO/utils/aoml.ts
index 76113990..79a5147d 100644
--- a/webAO/utils/aoml.ts
+++ b/webAO/utils/aoml.ts
@@ -75,7 +75,7 @@ const mlConfig = (iniContent: string) => {
closingStack.pop();
colorStack.pop();
if (keepChar) {
- currentSelector.innerHTML = letter;
+ currentSelector.textContent = letter;
}
} else if (startIdentifiers.has(letter)) {
const color = identifiers.get(letter).color.split(",");
@@ -87,10 +87,10 @@ const mlConfig = (iniContent: string) => {
const currentColor = `color: rgb(${r},${g},${b});`;
currentSelector.setAttribute("style", currentColor);
if (keepChar) {
- currentSelector.innerHTML = letter;
+ currentSelector.textContent = letter;
}
} else {
- currentSelector.innerHTML = letter;
+ currentSelector.textContent = letter;
if (colorStack.length === 0) {
currentSelector.className = `text_${defaultColor}`;
} else {
diff --git a/webAO/viewport/utils/handleICSpeaking.ts b/webAO/viewport/utils/handleICSpeaking.ts
index 84946d2d..79135548 100644
--- a/webAO/viewport/utils/handleICSpeaking.ts
+++ b/webAO/viewport/utils/handleICSpeaking.ts
@@ -317,7 +317,7 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
} else if (effectName.endsWith("strong")) {
intensity = 400;
}
- if (intensity < fg.childElementCount) fg.innerHTML = "";
+ if (intensity < fg.childElementCount) fg.replaceChildren();
else intensity = intensity - fg.childElementCount;
for (let i = 0; i < intensity; i++) {
@@ -331,11 +331,11 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
!badEffects.includes(effectName)
) {
(<HTMLLinkElement>document.getElementById("effect_css")).href = "";
- fg.innerHTML = "";
+ fg.replaceChildren();
const baseEffectUrl = `${AO_HOST}themes/default/effects/`;
fg.src = `${baseEffectUrl}${encodeURI(effectName)}.webp`;
} else {
- fg.innerHTML = "";
+ fg.replaceChildren();
fg.src = transparentPng;
}
@@ -353,7 +353,7 @@ export const handle_ic_speaking = async (playerChatMsg: ChatMsg) => {
const output: HTMLSpanElement[] = [];
for (const letter of client.viewport.getChatmsg().content) {
const currentSelector = document.createElement("span");
- currentSelector.innerHTML = letter;
+ currentSelector.textContent = letter;
currentSelector.className = `text_${COLORS[client.viewport.getChatmsg().color]}`;
output.push(currentSelector);
}
diff --git a/webAO/viewport/viewport.ts b/webAO/viewport/viewport.ts
index 1421c671..f13c3115 100644
--- a/webAO/viewport/viewport.ts
+++ b/webAO/viewport/viewport.ts
@@ -197,11 +197,11 @@ const viewport = (): Viewport => {
const textSpeeds = new Set(["{", "}"]);
// Changing Text Speed
- if (textSpeeds.has(characterElement.innerHTML)) {
+ if (textSpeeds.has(characterElement.textContent)) {
// Grab them all in a row
const MAX_SLOW_CHATSPEED = 120;
for (let i = textnow.length; i < chatmsg.content.length; i++) {
- const currentCharacter = chatmsg.parsed[i - 1].innerHTML;
+ const currentCharacter = chatmsg.parsed[i - 1].textContent;
if (currentCharacter === "}") {
if (chatmsg.speed > 0) {
chatmsg.speed -= 20;
@@ -219,12 +219,12 @@ const viewport = (): Viewport => {
}
if (
- characterElement.innerHTML === COMMAND_IDENTIFIER &&
- (commands.has(nextCharacterElement?.innerHTML) ||
- nextCharacterElement?.innerHTML === "p")
+ characterElement.textContent === COMMAND_IDENTIFIER &&
+ (commands.has(nextCharacterElement?.textContent) ||
+ nextCharacterElement?.textContent === "p")
) {
textnow = chatmsg.content.substring(0, textnow.length + 1);
- const commandChar = nextCharacterElement.innerHTML;
+ const commandChar = nextCharacterElement.textContent;
if (commandChar === "p") {
// Collect digits after \p for pause duration
@@ -233,9 +233,9 @@ const viewport = (): Viewport => {
let offset = 1;
while (
startPos + offset <= chatmsg.content.length &&
- /\d/.test(chatmsg.parsed[startPos + offset - 1]?.innerHTML || "")
+ /\d/.test(chatmsg.parsed[startPos + offset - 1]?.textContent || "")
) {
- digits += chatmsg.parsed[startPos + offset - 1].innerHTML;
+ digits += chatmsg.parsed[startPos + offset - 1].textContent;
textnow = chatmsg.content.substring(0, startPos + offset);
offset++;
}