Browse Source

initial commit

master
Amélia Liao 2 years ago
commit
39c9651d3d
11 changed files with 7520 additions and 0 deletions
  1. +4
    -0
      .gitignore
  2. +8
    -0
      Makefile
  3. +7046
    -0
      package-lock.json
  4. +22
    -0
      package.json
  5. +11
    -0
      rollup.config.js
  6. +1
    -0
      script.js
  7. +6
    -0
      src/external.d.ts
  8. +124
    -0
      src/jsx.ts
  9. +193
    -0
      src/main.tsx
  10. +88
    -0
      style.css
  11. +17
    -0
      tsconfig.json

+ 4
- 0
.gitignore View File

@ -0,0 +1,4 @@
.out/
.out-tsc
node_modules
/out.html

+ 8
- 0
Makefile View File

@ -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

+ 7046
- 0
package-lock.json
File diff suppressed because it is too large
View File


+ 22
- 0
package.json View File

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

+ 11
- 0
rollup.config.js View File

@ -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',
}
});

+ 1
- 0
script.js View File

@ -0,0 +1 @@
function e(e,t){return" ".repeat(null!=e?e:0)+t}var t={"&":"&amp;","<":"&lt;",">":"&gt;"};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());

+ 6
- 0
src/external.d.ts View File

@ -0,0 +1,6 @@
declare module JSX {
type Element = { toHtmlStr(): string };
interface IntrinsicElements {
[elemName: string]: any;
}
}

+ 124
- 0
src/jsx.ts View File

@ -0,0 +1,124 @@
function ind(x: number | undefined | null, str: string): string {
return " ".repeat(x ?? 0) + str;
};
var tagsToReplace: Record<string, string> = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
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;

+ 193
- 0
src/main.tsx View File

@ -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());

+ 88
- 0
style.css View File

@ -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;
}

+ 17
- 0
tsconfig.json View File

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

Loading…
Cancel
Save