@ -0,0 +1,4 @@ | |||
.out/ | |||
.out-tsc | |||
node_modules | |||
/out.html |
@ -0,0 +1,8 @@ | |||
script.js: .out/all.js | |||
cp $< $@ | |||
TS := $(shell find src/ -type f -name '*.ts' -or -name '*.tsx') | |||
.out/all.js: $(TS) | |||
npm run build | |||
mv .out/*.js .out/all.js |
@ -0,0 +1,22 @@ | |||
{ | |||
"name": "friendly-waffle", | |||
"version": "1.0.0", | |||
"description": "", | |||
"main": "built.js", | |||
"scripts": { | |||
"build": "rm -rf .out/*.js && tsc -b -v && node_modules/.bin/rollup -c rollup.config.js" | |||
}, | |||
"keywords": [], | |||
"author": "", | |||
"license": "ISC", | |||
"devDependencies": { | |||
"@open-wc/building-rollup": "^1.10.0", | |||
"deepmerge": "^4.2.2", | |||
"rimraf": "^3.0.2", | |||
"rollup": "^2.54.0" | |||
}, | |||
"dependencies": { | |||
"afinn-165": "^2.0.1", | |||
"chart.js": "^3.5.0" | |||
} | |||
} |
@ -0,0 +1,11 @@ | |||
import merge from 'deepmerge'; | |||
import { createBasicConfig } from '@open-wc/building-rollup'; | |||
const baseConfig = createBasicConfig(); | |||
export default merge(baseConfig, { | |||
input: '.out-tsc/src/main.js', | |||
output: { | |||
dir: '.out', | |||
} | |||
}); |
@ -0,0 +1 @@ | |||
function e(e,t){return" ".repeat(null!=e?e:0)+t}var t={"&":"&","<":"<",">":">"};class s{constructor(e){this.attrs={},this.content=[],this.name=e}setAttribute(e,t){this.attrs[e]=t}appendChild(...e){this.content.push(...e)}toHtmlStr(t){const s=[],n=[`<${this.name}`];for(const e of Object.keys(this.attrs)){if(!this.attrs.hasOwnProperty(e))continue;const t=this.attrs[e];void 0!==t&&("boolean"==typeof t&&t?n.push(e):n.push(`${e}="${t}"`))}if(0===this.content.length)return e(t,n.join(" "))+" />";s.push(e(t,n.join(" "))+">");for(const e of this.content)try{s.push(e.toHtmlStr((null!=t?t:0)+2))}catch(t){s.push(e.toString())}return s.push(e(t,`</${this.name}>`)),s.join("\n")}}class n{constructor(e){this.value=e}toHtmlStr(t){return e(t,this.value)}}const r=(e,t)=>{if("string"==typeof t)e.appendChild(new n(t.toString()));else if(t instanceof Array)t.forEach((t=>r(e,t)));else{if(void 0===t)return;e.appendChild(t)}};class a{static createElement(e,t,...n){if("string"!=typeof e)return e(t);{const a=new s(e),l=t||{};for(let e in l)if(e&&l.hasOwnProperty(e)){let t=l[e];!0===t?a.setAttribute(e,e):!1!==t&&null!=t&&a.setAttribute(e,t.toString())}for(let e=0;e<n.length;e++){let t=n[e];r(a,t)}return a}}}const{readFileSync:l}=require("fs");const c=function(e){const t=[];let s,n,r=[];for(const a of e)"status"!==a.type?void 0===s||void 0!==a.user.mode&&a.user.mode!==s.mode||a.user.name!==s.name?(0!==r.length&&t.push({user:s,type:null!=n?n:"message",messages:r}),r=[a],s=a.user,n=a.type):r.push(a):(t.push({user:s,type:null!=n?n:"message",messages:r}),s=void 0,n="status",r=[],t.push({type:a.type,messages:[a]}));return 0!==r.length&&("status"===n?t.push({user:void 0,type:n,messages:r}):void 0!==s&&t.push({user:s,type:null!=n?n:"message",messages:r})),t}(l(0).toString().split("\n").map((e=>e.trim())).filter((e=>"."!==e&&""!==e)).map(((e,t)=>{try{return function(e){const t=(e=e.trimEnd()).slice(0,5);switch(e[6]){case"-":return{time:t,type:"status",content:e.slice(10)};case"<":{const s=e[7];if("@"!==s&&"+"!==s&&" "!==s)throw`invalid mode: ${s}`;const n=e.slice(8).split(">")[0];return{time:t,type:"message",user:{mode:s,name:n},content:e.slice(10+n.length)}}case" ":{const s=e.slice(8).split(" ")[1];return{time:t,type:"action",user:{name:s},content:e.slice(10+s.length)}}}throw`couldn't parse message ${e}`}(e)}catch(e){throw`${e} ${t}`}}))),o=({message:e,inclTs:s})=>{let n=e.content.replace(/[&<>]/g,(e=>t[e]||e));return"action"===e.type&&(n="* "+n),a.createElement("div",{class:`mg-m mg-${e.type}`},void 0===s||!0===s?a.createElement("span",{class:"mg-ts"}," ",e.time," "):void 0,a.createElement("span",{class:`mg-txt mg-${e.type}`}," ",n," "))};const i=c.map((function(e){var t;const s=e.user;return console.error(e),void 0!==s?a.createElement("div",{class:"mg-user"},a.createElement("div",{class:"mg-pfp"},a.createElement("img",{src:`https://offtopia.org/pages/people/${s.name.toLocaleLowerCase()}.jpg`,alt:`${s.name}'s profile picture`})),a.createElement("div",{class:"mg-contents"},a.createElement("span",{class:"mg-ts-u"},a.createElement("span",{class:"mg-ts-u-u"}," ",null===(t=e.user)||void 0===t?void 0:t.name," "),a.createElement("span",{class:"mg-ts-u-ts"}," ",e.messages[0].time," ")),a.createElement(o,{message:e.messages[0],inclTs:!1}),e.messages.slice(1).map((e=>a.createElement(o,{message:e}))))):e.messages.length<1?a.createElement("span",null):a.createElement("div",{class:"mg-no-user"},e.messages.map((e=>a.createElement(o,{message:e}))))})),m=a.createElement("html",null,a.createElement("body",null,i,a.createElement("svg",{id:"squircle-container",width:"0",height:"0"},a.createElement("clipPath",{id:"squircle",clipPathUnits:"objectBoundingBox"},a.createElement("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"})))),a.createElement("style",null,l("style.css")));console.log(m.toHtmlStr()); |
@ -0,0 +1,6 @@ | |||
declare module JSX { | |||
type Element = { toHtmlStr(): string }; | |||
interface IntrinsicElements { | |||
[elemName: string]: any; | |||
} | |||
} |
@ -0,0 +1,124 @@ | |||
function ind(x: number | undefined | null, str: string): string { | |||
return " ".repeat(x ?? 0) + str; | |||
}; | |||
var tagsToReplace: Record<string, string> = { | |||
'&': '&', | |||
'<': '<', | |||
'>': '>' | |||
}; | |||
export function escapeHtmlShitty(x: string): string { | |||
return x.replace(/[&<>]/g, (tag) => tagsToReplace[tag] || tag); | |||
} | |||
export interface HtmlEnt { | |||
toHtmlStr(indent?: number): string; | |||
}; | |||
class HtmlElem implements HtmlEnt { | |||
public name: string; | |||
public attrs: { [id: string]: string | boolean | undefined } = {}; | |||
public content: HtmlEnt[] = []; | |||
constructor(n: string) { | |||
this.name = n; | |||
} | |||
setAttribute(id: string, val: string | boolean) { | |||
this.attrs[id] = val; | |||
} | |||
appendChild(...c: HtmlEnt[]) { | |||
this.content.push(...c); | |||
} | |||
toHtmlStr(indent?: number) { | |||
const out: string[] = []; | |||
const tag: string[] = [`<${this.name}`]; | |||
for (const key of Object.keys(this.attrs)) { | |||
if (!this.attrs.hasOwnProperty(key)) continue; | |||
const x = this.attrs[key]; | |||
if (x === undefined) continue; | |||
if (typeof x === "boolean" && !!x) { | |||
tag.push(key); | |||
} else { | |||
tag.push(`${key}="${x}"`); | |||
} | |||
} | |||
if (this.content.length === 0) { | |||
return ind(indent, tag.join(" ")) + " />"; | |||
} | |||
out.push(ind(indent, tag.join(" ")) + '>'); | |||
for (const e of this.content) { | |||
try { | |||
out.push(e.toHtmlStr((indent ?? 0) + 2)); | |||
} catch(exc) { | |||
out.push(e.toString()); | |||
} | |||
} | |||
out.push(ind(indent, `</${this.name}>`)); | |||
return out.join("\n"); | |||
} | |||
} | |||
class HtmlText implements HtmlEnt { | |||
public value: string; | |||
constructor(v: string) { | |||
this.value = v; | |||
} | |||
toHtmlStr(indent?: number) { | |||
return ind(indent, this.value); | |||
} | |||
} | |||
type Content = HtmlEnt | string | Content[] | undefined; | |||
const add = (element: HtmlElem, child: Content) => { | |||
if (typeof child === 'string') { | |||
element.appendChild(new HtmlText(child.toString())) | |||
} else if (child instanceof Array) { | |||
child.forEach((x) => add(element, x)); | |||
} else if (child === undefined) { | |||
return; | |||
} else { | |||
element.appendChild(child); | |||
} | |||
}; | |||
type JSXName<T> = string | ((props: T) => HtmlEnt); | |||
type ElemProps = { [id: string]: string | boolean } | |||
export class JSX { | |||
static createElement<T>(fn: (props: T) => HtmlEnt, props: T, ...content: Content[]): HtmlEnt; | |||
static createElement<P, T extends JSXName<P>>(name: T, arg: T extends 'string' ? ElemProps : P, ...content: Content[]): HtmlEnt { | |||
if (typeof name !== 'string') { | |||
return name(arg); | |||
} else { | |||
const element = new HtmlElem(name); | |||
const props = (arg as { [id: string]: string | boolean }) || {}; | |||
for (let name in props) { | |||
if (name && props.hasOwnProperty(name)) { | |||
let value = props[name]; | |||
if (value === true) { | |||
element.setAttribute(name, name); | |||
} else if (value !== false && value != null) { | |||
element.setAttribute(name, value.toString()); | |||
} | |||
} | |||
} | |||
for (let i = 0; i < content.length; i++) { | |||
let child = content[i]; | |||
add(element, child); | |||
} | |||
return element; | |||
} | |||
} | |||
}; | |||
export default JSX; |
@ -0,0 +1,193 @@ | |||
import { escapeHtmlShitty, HtmlEnt, JSX } from './jsx'; | |||
const { readFileSync } = require("fs"); | |||
const contents = readFileSync(0); | |||
const lines: string[] = contents.toString().split('\n'); | |||
type IrcUser = { | |||
mode?: '+' | '@' | ' ', | |||
name: string | |||
}; | |||
type IrcMessage = { | |||
time: string, | |||
type: 'status', | |||
content: string | |||
} | { | |||
time: string, | |||
type: 'action', | |||
user: IrcUser, | |||
content: string | |||
} | { | |||
time: string, | |||
type: 'message', | |||
user: IrcUser, | |||
content: string | |||
}; | |||
function parse(s: string): IrcMessage { | |||
s = s.trimEnd(); | |||
const time = s.slice(0, 5); | |||
switch(s[6]) { | |||
case '-': | |||
return { | |||
time, | |||
type: 'status', | |||
content: 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: s.slice(10 + name.length) | |||
} | |||
} | |||
case ' ': { | |||
const name = s.slice(8).split(' ')[1]; | |||
return { | |||
time, | |||
type: 'action', | |||
user: { name }, | |||
content: s.slice(10 + name.length) | |||
} | |||
} | |||
} | |||
throw `couldn't 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", | |||
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") { | |||
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 Message = ( {message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => { | |||
let txt = escapeHtmlShitty(message.content); | |||
if (message.type === "action") { | |||
txt = "* " + txt; | |||
} | |||
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}` }> { txt } </span> | |||
</div> | |||
} | |||
function renderMessageGroup(m: MsgGroup): HtmlEnt { | |||
const u = m.user; | |||
console.error(m); | |||
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"> { m.messages[0].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 <span /> | |||
} | |||
return <div class="mg-no-user"> | |||
{ m.messages.map(x => <Message message={x} />) } | |||
</div>; | |||
} | |||
} | |||
const rendered = groups.map(renderMessageGroup); | |||
const elem = <html> | |||
<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()); |
@ -0,0 +1,88 @@ | |||
html { | |||
font-family: sans-serif; | |||
padding: 2px; | |||
--pfp-size: 48px; | |||
--font-size: 14pt; | |||
--ts-font-size: calc(var(--font-size) - 2pt); | |||
} | |||
body { | |||
font-size: var(--font-size); | |||
padding-left: 0.8em; | |||
} | |||
.mg-user { | |||
display: flex; | |||
flex-direction: row; | |||
flex-wrap: nowrap; | |||
margin-top: 0.75em; | |||
margin-bottom: 0.75em; | |||
} | |||
.mg-pfp { | |||
margin-right: 0.75em; | |||
} | |||
.mg-user > .mg-pfp > img { | |||
width: var(--pfp-size); | |||
height: var(--pfp-size); | |||
clip-path: url(#squircle); | |||
} | |||
.mg-user > .mg-contents { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: flex-start; | |||
min-height: var(--pfp-size); | |||
gap: 0.33em; | |||
} | |||
.mg-user > .mg-contents > .mg-ts-u > .mg-ts-u-u { | |||
font-weight: bold; | |||
} | |||
.mg-user > .mg-contents > .mg-ts-u > .mg-ts-u-ts { | |||
font-weight: lighter; | |||
color: #666; | |||
font-size: var(--ts-font-size); | |||
} | |||
.mg-user > .mg-contents > .mg-m .mg-ts { | |||
display: none; | |||
} | |||
.mg-user > .mg-contents > .mg-m:hover .mg-ts { | |||
display: unset; | |||
position: absolute; | |||
margin-left: calc(-1 * var(--pfp-size) - 0.75em); | |||
font-size: var(--ts-font-size); | |||
font-weight: lighter; | |||
color: #666; | |||
} | |||
.mg-m > .mg-action { | |||
font-style: italic; | |||
} | |||
.mg-no-user > .mg-status { | |||
margin-top: 1em; | |||
margin-bottom: 1em; | |||
font-size: 0pt; | |||
font-weight: bold; | |||
} | |||
.mg-no-user > .mg-status > .mg-ts { | |||
font-size: 12pt; | |||
width: var(--pfp-size); | |||
display: inline-block; | |||
margin-right: 14px; | |||
} | |||
.mg-no-user > .mg-status > .mg-txt { | |||
font-size: 14pt; | |||
} |
@ -0,0 +1,17 @@ | |||
{ | |||
"compilerOptions": { | |||
"target": "es2018", | |||
"module": "esnext", | |||
"moduleResolution": "node", | |||
"noEmitOnError": true, | |||
"lib": ["es2017", "dom"], | |||
"strict": true, | |||
"esModuleInterop": false, | |||
"outDir": ".out-tsc", | |||
"rootDir": "./", | |||
"jsx": "react", | |||
"reactNamespace": "JSX", | |||
} | |||
, | |||
"include": ["./src/**/*.ts", "./src/**/*.tsx"] | |||
} |