|
import { escapeHtmlShitty, HtmlEnt, JSX } from './jsx';
|
|
|
|
const { readFileSync } = require("fs");
|
|
const contents = readFileSync(0);
|
|
const lines: string[] = contents.toString().split('\n');
|
|
|
|
type MessageFrag = {
|
|
type: 'url',
|
|
link: string
|
|
} | {
|
|
type: 'text',
|
|
content: string
|
|
} | {
|
|
type: 'error',
|
|
content: string
|
|
};
|
|
|
|
type IrcUser = {
|
|
mode?: '+' | '@' | ' ',
|
|
name: string
|
|
};
|
|
|
|
type IrcMessage = {
|
|
time: string,
|
|
type: 'status',
|
|
content: MessageFrag[]
|
|
} | {
|
|
time: string,
|
|
type: 'action',
|
|
user: IrcUser,
|
|
content: MessageFrag[]
|
|
} | {
|
|
time: string,
|
|
type: 'message',
|
|
user: IrcUser,
|
|
content: MessageFrag[]
|
|
} | {
|
|
type: 'day_change'
|
|
} | {
|
|
type: 'redacted'
|
|
};
|
|
|
|
const URL = /(((?:https?|gopher):\/\/|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/|spotify:track:)((?:\(?[^\s()<>]+\)?)*[^ \s`!\[\]{};:\'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))/i;
|
|
|
|
function parseMessage(m: string): MessageFrag[] {
|
|
m = m.trimEnd();
|
|
const parts: MessageFrag[] = [];
|
|
let match: RegExpMatchArray | null;
|
|
while ((match = m.match(URL)) != null) {
|
|
parts.push({ type: 'text', content: m.slice(0, match.index) });
|
|
parts.push({ type: 'url', link: match[0] });
|
|
m = m.slice((match.index ?? 0) + match[0].length);
|
|
}
|
|
if (m !== "") {
|
|
parts.push({ type: 'text', content: m });
|
|
}
|
|
return parts;
|
|
}
|
|
|
|
function parse(s: string): IrcMessage {
|
|
s = s.trimEnd();
|
|
if (s.startsWith("--- Day changed")) {
|
|
return {
|
|
type: "day_change"
|
|
};
|
|
} else if (s === "[REDACTED]") {
|
|
return {
|
|
type: "redacted"
|
|
};
|
|
}
|
|
|
|
const time = s.slice(0, 5);
|
|
switch (s[6]) {
|
|
case '-':
|
|
return {
|
|
time,
|
|
type: 'status',
|
|
content: parseMessage(s.slice(10))
|
|
};
|
|
case '<': {
|
|
const mode = s[7];
|
|
if (mode !== '@' && mode !== '+' && mode !== ' ') throw `invalid mode: ${mode}`;
|
|
const name = s.slice(8).split('>')[0];
|
|
return {
|
|
time,
|
|
type: 'message',
|
|
user: { mode, name },
|
|
content: parseMessage(s.slice(10 + name.length))
|
|
}
|
|
}
|
|
case ' ': {
|
|
const name = s.slice(8).split(' ')[1];
|
|
return {
|
|
time,
|
|
type: 'action',
|
|
user: { name },
|
|
content: parseMessage(s.slice(10 + name.length))
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
time: "error",
|
|
type: 'status',
|
|
content: [{
|
|
type: 'error',
|
|
content: `could not parse message: ${s}`
|
|
}]
|
|
};
|
|
};
|
|
|
|
const messages = lines.map(l => l.trim()).filter(l => l !== '.' && l !== "").map((v, i) => { try { return parse(v) } catch (e) { throw `${e} ${i}` } });
|
|
|
|
type MsgGroup = {
|
|
user?: IrcUser,
|
|
type: "status" | "message" | "action" | "day_change" | "redacted",
|
|
messages: IrcMessage[]
|
|
}
|
|
|
|
function group(messages: IrcMessage[]): MsgGroup[] {
|
|
const groups: MsgGroup[] = [];
|
|
|
|
let group: IrcMessage[] = [];
|
|
let discrim: IrcUser | undefined = undefined;
|
|
let type: "status" | "action" | "message" | undefined = undefined;
|
|
|
|
for (const m of messages) {
|
|
if (m.type === "status" || m.type === "day_change" || m.type == "redacted") {
|
|
groups.push({
|
|
user: discrim,
|
|
type: type ?? "message",
|
|
messages: group
|
|
});
|
|
|
|
discrim = undefined;
|
|
type = "status";
|
|
group = [];
|
|
groups.push({
|
|
type: m.type,
|
|
messages: [m]
|
|
})
|
|
continue;
|
|
} else if (discrim !== undefined && (m.user.mode === undefined || m.user.mode === discrim.mode) && m.user.name === discrim.name) {
|
|
group.push(m)
|
|
} else {
|
|
if (group.length !== 0)
|
|
groups.push({
|
|
user: discrim,
|
|
type: type ?? "message",
|
|
messages: group
|
|
});
|
|
group = [m];
|
|
discrim = m.user;
|
|
type = m.type;
|
|
}
|
|
}
|
|
|
|
if (group.length !== 0) {
|
|
if (type === "status") {
|
|
groups.push({
|
|
user: undefined,
|
|
type,
|
|
messages: group
|
|
})
|
|
} else if (discrim !== undefined) {
|
|
groups.push({
|
|
user: discrim,
|
|
type: type ?? "message",
|
|
messages: group
|
|
});
|
|
}
|
|
}
|
|
|
|
return groups;
|
|
}
|
|
|
|
const groups = group(messages);
|
|
|
|
const render = (c: MessageFrag): JSX.Element => {
|
|
if (c.type === "text") {
|
|
return JSX.createTextNode(escapeHtmlShitty(c.content));
|
|
} else if (c.type === "url") {
|
|
return <a href={c.link}>{escapeHtmlShitty(c.link)}</a>;
|
|
} else if (c.type === "error") {
|
|
return <span style="color: red; font-family: monospace">{escapeHtmlShitty(c.content)}</span>;
|
|
} else {
|
|
throw "impossible!" // thanks TypeScript
|
|
}
|
|
}
|
|
|
|
const Message = ({ message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => {
|
|
if (message.type === "day_change") {
|
|
return <div class="mg-sep">
|
|
<span>Today</span>
|
|
</div>
|
|
};
|
|
|
|
if (message.type === "redacted") {
|
|
return <div class="mg-redacted">
|
|
[REDACTED]
|
|
</div>;
|
|
}
|
|
|
|
return <div class={`mg-m mg-${message.type}`}>
|
|
{
|
|
(inclTs === undefined || inclTs === true) ? <span class="mg-ts"> {message.time} </span> : undefined
|
|
}
|
|
<span class={`mg-txt mg-${message.type}`}>
|
|
{ message.content.map(c => render(c)) }
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
function renderMessageGroup(m: MsgGroup): HtmlEnt {
|
|
const u = m.user;
|
|
const m0 = m.messages[0];
|
|
|
|
if (u !== undefined) {
|
|
return <div class="mg-user">
|
|
<div class="mg-pfp">
|
|
<img src={`https://offtopia.org/pages/people/${u.name.toLocaleLowerCase()}.jpg`} alt={`${u.name}'s profile picture`} />
|
|
</div>
|
|
<div class="mg-contents">
|
|
<span class="mg-ts-u">
|
|
<span class="mg-ts-u-u"> {m.user?.name} </span>
|
|
<span class="mg-ts-u-ts"> {(m0.type === "day_change" || m0.type === "redacted") ? "what" : m0.time} </span>
|
|
</span>
|
|
|
|
<Message message={m.messages[0]} inclTs={false} />
|
|
|
|
{
|
|
m.messages.slice(1).map(x =>
|
|
<Message message={x} />
|
|
)
|
|
}
|
|
</div>
|
|
</div>
|
|
} else {
|
|
if (m.messages.length < 1) {
|
|
return <hr style="display: none;" />
|
|
}
|
|
|
|
return <div class="mg-no-user">
|
|
{m.messages.map(x => <Message message={x} />)}
|
|
</div>;
|
|
}
|
|
}
|
|
|
|
const rendered = groups.map(renderMessageGroup);
|
|
|
|
const elem = <html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"></meta>
|
|
</head>
|
|
<body>
|
|
{rendered}
|
|
<svg id="squircle-container" width="0" height="0">
|
|
<clipPath id="squircle" clipPathUnits="objectBoundingBox">
|
|
<path fill="red" stroke="none" d="M 0,0.5 C 0,0 0,0 0.5,0 S 1,0 1,0.5 1,1 0.5,1 0,1 0,0.5" />
|
|
</clipPath>
|
|
</svg>
|
|
</body>
|
|
<style>
|
|
{readFileSync("style.css")}
|
|
</style>
|
|
|
|
</html>;
|
|
|
|
console.log(elem.toHtmlStr());
|