Browse Source

parse URLs

master
Amélia Liao 2 years ago
parent
commit
c5f9a64357
4 changed files with 64 additions and 28 deletions
  1. +2
    -1
      .gitignore
  2. +0
    -1
      script.js
  3. +7
    -3
      src/jsx.ts
  4. +55
    -23
      src/main.tsx

+ 2
- 1
.gitignore View File

@ -1,4 +1,5 @@
.out/
.out-tsc
node_modules
/out.html
/out.html
/script.js

+ 0
- 1
script.js View File

@ -1 +0,0 @@
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());

+ 7
- 3
src/jsx.ts View File

@ -1,5 +1,5 @@
function ind(x: number | undefined | null, str: string): string {
return " ".repeat(x ?? 0) + str;
function ind(_: number | undefined | null, str: string): string {
return str;
};
var tagsToReplace: Record<string, string> = {
@ -60,7 +60,7 @@ class HtmlElem implements HtmlEnt {
}
}
out.push(ind(indent, `</${this.name}>`));
return out.join("\n");
return out.join("");
}
}
@ -93,6 +93,10 @@ type JSXName<T> = string | ((props: T) => HtmlEnt);
type ElemProps = { [id: string]: string | boolean }
export class JSX {
static createTextNode(t: string): HtmlEnt {
return new HtmlText(t);
}
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') {


+ 55
- 23
src/main.tsx View File

@ -4,6 +4,14 @@ 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 IrcUser = {
mode?: '+' | '@' | ' ',
name: string
@ -12,28 +20,46 @@ type IrcUser = {
type IrcMessage = {
time: string,
type: 'status',
content: string
content: MessageFrag[]
} | {
time: string,
type: 'action',
user: IrcUser,
content: string
content: MessageFrag[]
} | {
time: string,
type: 'message',
user: IrcUser,
content: string
content: MessageFrag[]
};
const URLScheme = /https?:\/\/[\w\.\/\?\=\-]+/;
function parseMessage(m: string): MessageFrag[] {
m = m.trimEnd();
const parts: MessageFrag[] = [];
let match: RegExpMatchArray | null;
while ((match = m.match(URLScheme)) != 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 });
}
console.error(parts);
return parts;
}
function parse(s: string): IrcMessage {
s = s.trimEnd();
const time = s.slice(0, 5);
switch(s[6]) {
switch (s[6]) {
case '-':
return {
time,
type: 'status',
content: s.slice(10)
content: parseMessage(s.slice(10))
};
case '<': {
const mode = s[7];
@ -43,7 +69,7 @@ function parse(s: string): IrcMessage {
time,
type: 'message',
user: { mode, name },
content: s.slice(10 + name.length)
content: parseMessage(s.slice(10 + name.length))
}
}
case ' ': {
@ -52,14 +78,14 @@ function parse(s: string): IrcMessage {
time,
type: 'action',
user: { name },
content: s.slice(10 + name.length)
content: parseMessage(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}` } });
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,
@ -126,37 +152,43 @@ function group(messages: IrcMessage[]): MsgGroup[] {
const groups = group(messages);
const Message = ( {message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => {
let txt = escapeHtmlShitty(message.content);
if (message.type === "action") {
txt = "* " + txt;
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 {
throw "impossible!" // thanks TypeScript
}
return <div class={ `mg-m mg-${message.type}` }>
}
const Message = ({ message, inclTs }: { message: IrcMessage, inclTs?: boolean }) => {
const prefix = message.type === "action" ? "* " : "";
return <div class={`mg-m mg-${message.type}`}>
{
(inclTs === undefined || inclTs === true) ? <span class="mg-ts"> { message.time } </span> : undefined
(inclTs === undefined || inclTs === true) ? <span class="mg-ts"> {message.time} </span> : undefined
}
<span class={ `mg-txt mg-${message.type}` }> { txt } </span>
<span class={`mg-txt mg-${message.type}`}> {prefix} {message.content.map(c => render(c))} </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`} />
<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 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 =>
m.messages.slice(1).map(x =>
<Message message={x} />
)
}
@ -168,7 +200,7 @@ function renderMessageGroup(m: MsgGroup): HtmlEnt {
}
return <div class="mg-no-user">
{ m.messages.map(x => <Message message={x} />) }
{m.messages.map(x => <Message message={x} />)}
</div>;
}
}
@ -177,7 +209,7 @@ const rendered = groups.map(renderMessageGroup);
const elem = <html>
<body>
{ rendered }
{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" />
@ -185,7 +217,7 @@ const elem = <html>
</svg>
</body>
<style>
{ readFileSync("style.css") }
{readFileSync("style.css")}
</style>
</html>;


Loading…
Cancel
Save