@ -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"] | |||||
} |