From 0be83ae8ceb5839e0086354a40d009ddea2ac494 Mon Sep 17 00:00:00 2001
From: gnuxie
Date: Fri, 13 Jan 2023 19:01:58 +0000
Subject: [PATCH] We can now render to Matrix using JSX.
---
src/appservice/bot/ListCommand.tsx | 97 +++++++++++++++++++
.../interface-manager/DeadDocument.ts | 54 +++++++----
.../interface-manager/DeadDocumentHtml.ts | 6 ++
.../interface-manager/DeadDocumentMarkdown.ts | 45 +++++++--
.../interface-manager/DeadDocumentMatrix.ts | 4 +
src/commands/interface-manager/JSXFactory.ts | 34 ++++---
6 files changed, 201 insertions(+), 39 deletions(-)
create mode 100644 src/appservice/bot/ListCommand.tsx
diff --git a/src/appservice/bot/ListCommand.tsx b/src/appservice/bot/ListCommand.tsx
new file mode 100644
index 00000000..e8b2b407
--- /dev/null
+++ b/src/appservice/bot/ListCommand.tsx
@@ -0,0 +1,97 @@
+/**
+ * Copyright (C) 2022 Gnuxie
+ * All rights reserved.
+ */
+
+import { defineMatrixInterfaceAdaptor, MatrixContext, MatrixInterfaceAdaptor } from '../../commands/interface-manager/MatrixInterfaceAdaptor';
+import { MatrixSendClient } from '../../MatrixEmitter';
+import { UnstartedMjolnir } from '../MjolnirManager';
+import { BaseFunction, defineInterfaceCommand } from '../../commands/interface-manager/InterfaceCommand';
+import { findPresentationType, paramaters } from '../../commands/interface-manager/ParamaterParsing';
+import { AppserviceBaseExecutor } from './AppserviceCommandHandler';
+import { UserID } from 'matrix-bot-sdk';
+import { CommandError, CommandResult } from '../../commands/interface-manager/Validation';
+import { tickCrossRenderer } from '../../commands/interface-manager/MatrixHelpRenderer';
+import { JSXFactory } from '../../commands/interface-manager/JSXFactory';
+import { renderMatrixAndSend } from '../../commands/interface-manager/DeadDocumentMatrix';
+
+/**
+ * There is ovbiously something we're doing very wrong here,
+ * all just to satisfy type contstraints.
+ * Really there should only be `defineApplicationCommand`
+ * and `defineMatrixInterfaceCommand` but we have to do this
+ * long winded dance just to make type contstraints work.
+ * There might be a better way, but at what cost.
+ */
+
+const listUnstarted = defineInterfaceCommand({
+ designator: ["list", "unstarted"],
+ table: "appservice bot",
+ paramaters: paramaters([]),
+ command: async function() {
+ return CommandResult.Ok(this.appservice.mjolnirManager.getUnstartedMjolnirs());
+ },
+ summary: "List any Mjolnir that failed to start."
+});
+
+// Hmm what if leter on we used OL and the numbers could be a presentation type
+// and be used similar to like #=1 and #1.
+defineMatrixInterfaceAdaptor({
+ interfaceCommand: listUnstarted,
+ renderer: async function(this: MatrixInterfaceAdaptor, client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult) {
+ tickCrossRenderer.call(this, client, commandRoomId, event, result); // don't await, it doesn't really matter.
+ if (result.isErr()) {
+ return; // just let the default handler deal with it.
+ }
+ const unstarted = result.ok;
+ await renderMatrixAndSend(
+
+ Unstarted Mjolnir: {unstarted.length}
+
+ {unstarted.map(mjolnir => {
+ return -
+ {mjolnir.mjolnirRecord.owner},
+
{mjolnir.mxid.toString()}
+ {mjolnir.failCode}:
+
+ {mjolnir.cause}
+
+ })}
+
+
,
+ commandRoomId,
+ event,
+ client
+ );
+ }
+})
+
+// We need a "default" adaptor that needs to be explicitly defined still
+// (since you need to know if you have not created an adaptor)
+// but can be composed onto the end of existing adaptors easily
+// e.g. read recipt and tick vs cross.
+const restart = defineInterfaceCommand({
+ designator: ["restart"],
+ table: "appservice bot",
+ paramaters: paramaters([
+ {
+ name: "mjolnir",
+ acceptor: findPresentationType("UserID"),
+ }
+ ]),
+ command: async(context, mjolnirId: UserID): Promise> => {
+ const mjolnirManager = context.appservice.mjolnirManager;
+ const mjolnir = mjolnirManager.findUnstartedMjolnir(mjolnirId.localpart);
+ if (mjolnir?.mjolnirRecord === undefined) {
+ return CommandError.Result(`We can't find the unstarted mjolnir ${mjolnirId}, is it running?`);
+ }
+ await mjolnirManager.startMjolnir(mjolnir?.mjolnirRecord);
+ return CommandResult.Ok(true);
+ },
+ summary: "Attempt to restart a Mjolnir."
+})
+
+defineMatrixInterfaceAdaptor({
+ interfaceCommand: restart,
+ renderer: tickCrossRenderer
+})
diff --git a/src/commands/interface-manager/DeadDocument.ts b/src/commands/interface-manager/DeadDocument.ts
index 5018b089..c12f6b97 100644
--- a/src/commands/interface-manager/DeadDocument.ts
+++ b/src/commands/interface-manager/DeadDocument.ts
@@ -37,6 +37,8 @@ export enum NodeTag {
UnorderedList = 'ul',
ListItem = 'li',
LineBreak = 'br',
+ BoldFace = 'b',
+ ItalicFace = 'i',
}
/**
@@ -57,7 +59,7 @@ export function addChild(this: DeadDocumentN
}
export function getChildren(this: DeadDocumentNode): (DocumentNode|LeafNode)[] {
- return this.children;
+ return [...this.children];
}
export function getFirstChild(this: DeadDocumentNode): DocumentNode|LeafNode|undefined {
@@ -271,7 +273,7 @@ export class FringeWalker {
this.dynamicEnvironment.pop(node.node);
return node.node;
}
- while(!COMMITTABLE_NODES.has(this.stream.peekItem()?.node.tag)) {
+ while(this.stream.peekItem() && !COMMITTABLE_NODES.has(this.stream.peekItem().node.tag)) {
const node = this.stream.readItem();
switch(node.type) {
case FringeType.Pre:
@@ -293,35 +295,51 @@ export class FringeWalker {
if (this.stream.peekItem() === undefined) {
return undefined;
}
- const node = this.stream.readItem();
- postNode(node);
+ const node = postNode(this.stream.readItem());
this.commitHook(node, this.context);
return node;
}
}
-export interface TagDynamicEnvironmentEntry {
- node: DocumentNode,
- value: any,
+export class TagDynamicEnvironmentEntry {
+ constructor(
+ public readonly node: DocumentNode,
+ public readonly value: any,
+ public readonly previous: undefined|TagDynamicEnvironmentEntry,
+ ) {
+
+ }
}
+// OK we wrote this wrong
+// TagDynamicEnvironmentEntries need to be associated with a node
+// but not the variables themselves...
+// otherwise how can a list item access the indentation width managed by a list or
+// ordered list, all three of which have different tags
export class TagDynamicEnvironment {
- private readonly environments = new Map();
+ private readonly environments = new Map();
- public getEnvironment(tag: NodeTag): TagDynamicEnvironmentEntry[] {
- return this.environments.get(tag)
- ?? ((bootstrap: TagDynamicEnvironmentEntry[]) => (this.environments.set(tag, bootstrap), bootstrap))([]);
+ public getVariable(variableName: string): any|undefined {
+ const variableEntry = this.environments.get(variableName);
+ if (variableEntry) {
+ return variableEntry.value;
+ } else {
+ return undefined;
+ }
}
- public bind(node: DocumentNode, value: any) {
- const entry = this.getEnvironment(node.tag);
- entry.push({ node, value });
+ public bind(variableName: string, node: DocumentNode, value: any): any {
+ const entry = this.environments.get(variableName);
+ const newEntry = new TagDynamicEnvironmentEntry(node, value, entry);
+ this.environments.set(node.tag, newEntry);
+ return value
}
- public pop(node: DocumentNode) {
- const entry = this.getEnvironment(node.tag);
- if (Object.is(entry.at(0), node)) {
- entry.pop();
+ public pop(node: DocumentNode): void {
+ for (const [variableName, environment] of this.environments.entries()) {
+ if (Object.is(environment?.node, node)) {
+ this.environments.set(variableName, environment?.previous);
+ }
}
}
}
diff --git a/src/commands/interface-manager/DeadDocumentHtml.ts b/src/commands/interface-manager/DeadDocumentHtml.ts
index ff6201a5..765cd6ba 100644
--- a/src/commands/interface-manager/DeadDocumentHtml.ts
+++ b/src/commands/interface-manager/DeadDocumentHtml.ts
@@ -41,4 +41,10 @@ HTML_RENDERER.registerRenderer'),
+).registerInnerNode(NodeTag.BoldFace,
+ staticString(''),
+ staticString('')
+).registerInnerNode(NodeTag.ItalicFace,
+ staticString(''),
+ staticString('')
);
diff --git a/src/commands/interface-manager/DeadDocumentMarkdown.ts b/src/commands/interface-manager/DeadDocumentMarkdown.ts
index f4c6e47b..8a41d8d4 100644
--- a/src/commands/interface-manager/DeadDocumentMarkdown.ts
+++ b/src/commands/interface-manager/DeadDocumentMarkdown.ts
@@ -82,14 +82,26 @@ export class PagedDuplexStream {
}
public peekPage(): string|undefined {
+ // We consider a page "ready" when it is no longer the current page.
+ if (this.pages.length < 2) {
+ return undefined;
+ }
return this.pages.at(0);
}
public readPage(): string|undefined {
+ // We consider a page "ready" when it is no longer the current page.
+ if (this.pages.length < 2) {
+ return undefined;
+ }
return this.pages.shift();
}
}
+enum MarkdownVariables {
+ IndentationLevel = "indentation level"
+}
+
/**
* Hoping to replace this soon? or subclass
* FringeWalker so this is just a stream?
@@ -107,12 +119,14 @@ export function staticString(string: string): FringeInnerRenderFunction {
+ const value = environment.getVariable(MarkdownVariables.IndentationLevel);
+ if (!Number.isInteger(value)) {
+ throw new TypeError(`Cannot render the list ${node.tag} because someone clobbered the dynamic environment, should only have integers. Did you forget to enclose in or ?`)
+ } else {
+ return value;
+ }
+ })();
+
context.output.writeString('\n');
- for (let i = 0; i < entry?.value; i++) {
+ for (let i = 0; i < indentationLevel; i++) {
context.output.writeString(' ');
}
context.output.writeString(' * ');
@@ -163,4 +182,10 @@ MARKDOWN_RENDERER.registerRenderer o.ensureNewPage());
+ if (outputs.some(o => o.peekPage())) {
+ await cb(markdownOutput.readPage()!, htmlOutput.readPage()!);
+ }
}
export async function renderMatrixAndSend(node: DocumentNode, roomId: string, event: any, client: MatrixSendClient): Promise {
diff --git a/src/commands/interface-manager/JSXFactory.ts b/src/commands/interface-manager/JSXFactory.ts
index e9a36743..a1c54cba 100644
--- a/src/commands/interface-manager/JSXFactory.ts
+++ b/src/commands/interface-manager/JSXFactory.ts
@@ -5,22 +5,21 @@
import { DocumentNode, LeafNode, makeDocumentNode, makeLeafNode, NodeTag, TextNode } from "./DeadDocument";
-declare global {
- namespace JSX {
- interface IntrinsicElements {
- [elemName: string]: any;
- }
- }
-}
+type rawJSX = DocumentNode|LeafNode|string|number|Array;
export function JSXFactory(tag: NodeTag, properties: any, ...rawChildren: (DocumentNode|LeafNode|string)[]) {
const node = makeDocumentNode(tag);
- const ensureChild = (rawChild: DocumentNode|LeafNode|string) => {
+ const ensureChild = (rawChild: rawJSX) => {
if (typeof rawChild === 'string') {
- return makeLeafNode(NodeTag.TextNode, node, rawChild);
- } else {
+ makeLeafNode(NodeTag.TextNode, node, rawChild);
+ } else if (typeof rawChild === 'number') {
+ makeLeafNode(NodeTag.TextNode, node, (rawChild as number).toString());
+ } else if (Array.isArray(rawChild)) {
+ rawChild.forEach(ensureChild);
+ } else if (typeof rawChild.leafNode === 'boolean'){
node.addChild(rawChild);
- return rawChild;
+ } else {
+ throw new TypeError(`Unexpected raw child ${JSON.stringify(rawChild)}`)
}
}
rawChildren.forEach(ensureChild);
@@ -34,3 +33,16 @@ namespace JSXFactory {
}
}
+/**
+ * Pisses me off that tooling is too dumb to use the above
+ * https://www.typescriptlang.org/docs/handbook/declaration-merging.html
+ * https://www.typescriptlang.org/tsconfig#jsxFactory
+ */
+declare global {
+ export namespace JSX {
+ export interface IntrinsicElements {
+ [elemName: string]: any;
+ }
+ }
+}
+