We can now render to Matrix using JSX.

This commit is contained in:
gnuxie committed 2023-01-14 18:33:06 +00:00
1 parent 4b7e6637fa
commit 0be83ae8ce
6 files changed
+201 -39

No files matched your search

+97
View File
@@ -0,0 +1,97 @@
/**
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
* 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<AppserviceBaseExecutor>({
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<MatrixContext, BaseFunction>, client: MatrixSendClient, commandRoomId: string, event: any, result: CommandResult<UnstartedMjolnir[]>) {
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(
<p>
<b>Unstarted Mjolnir: {unstarted.length}</b>
<ul>
{unstarted.map(mjolnir => {
return <li>
{mjolnir.mjolnirRecord.owner},
<code>{mjolnir.mxid.toString()}</code>
<code>{mjolnir.failCode}</code>:
<br/>
{mjolnir.cause}
</li>
})}
</ul>
</p>,
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<AppserviceBaseExecutor>({
designator: ["restart"],
table: "appservice bot",
paramaters: paramaters([
{
name: "mjolnir",
acceptor: findPresentationType("UserID"),
}
]),
command: async(context, mjolnirId: UserID): Promise<CommandResult<true>> => {
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
})
+36 -18
View File
@@ -37,6 +37,8 @@ export enum NodeTag {
UnorderedList = 'ul',
ListItem = 'li',
LineBreak = 'br',
BoldFace = 'b',
ItalicFace = 'i',
}
/**
@@ -57,7 +59,7 @@ export function addChild<Node extends DocumentNode|LeafNode>(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<Context> {
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<Context> {
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<NodeTag, TagDynamicEnvironmentEntry[]>();
private readonly environments = new Map<string, TagDynamicEnvironmentEntry|undefined>();
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);
}
}
}
}
@@ -41,4 +41,10 @@ HTML_RENDERER.registerRenderer<FringeLeafRenderFunction<TransactionalOutputConte
).registerInnerNode(NodeTag.LineBreak,
blank,
staticString('<br/>'),
).registerInnerNode(NodeTag.BoldFace,
staticString('<b>'),
staticString('</b>')
).registerInnerNode(NodeTag.ItalicFace,
staticString('<i>'),
staticString('</i>')
);
@@ -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<Transact
}
export function blank() { }
export function incrementDynamicEnvironment(_fringe: FringeType, node: DocumentNode, _context: TransactionalOutputContext, environment: TagDynamicEnvironment) {
const entry = environment.getEnvironment(node.tag).at(0);
if (entry) {
if (!Number.isInteger(entry.value)) {
throw new TypeError(`${node.tag} should not have a dynamic environment entry that isn't an integer`);
const value = environment.getVariable(MarkdownVariables.IndentationLevel);
if (value) {
if (!Number.isInteger(value)) {
throw new TypeError(`${MarkdownVariables.IndentationLevel} should not have a dynamic environment entry that isn't an integer`);
}
environment.bind(node, entry.value + 1);
environment.bind(MarkdownVariables.IndentationLevel, node, value + 1);
} else {
environment.bind(MarkdownVariables.IndentationLevel, node, 1);
}
}
@@ -149,12 +163,17 @@ MARKDOWN_RENDERER.registerRenderer<FringeLeafRenderFunction<TransactionalOutputC
blank
).registerInnerNode(NodeTag.ListItem,
function(_fringe: FringeType, node: DocumentNode, context: TransactionalOutputContext, environment: TagDynamicEnvironment) {
const entry = environment.getEnvironment(node.tag).at(0);
if (!Number.isInteger(entry?.value)) {
throw new TypeError(`Cannot render the list ${node.tag} because someone clobbered the dynamic environment, should only have integers`)
}
const indentationLevel: number = (() => {
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 <ul> or <ol>?`)
} 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<FringeLeafRenderFunction<TransactionalOutputC
).registerInnerNode(NodeTag.LineBreak,
blank,
staticString('\n')
).registerInnerNode(NodeTag.BoldFace,
staticString('**'),
staticString('**')
).registerInnerNode(NodeTag.ItalicFace,
staticString('*'),
staticString('*')
);
@@ -54,6 +54,10 @@ export async function renderMatrix(node: DocumentNode, cb: SendMatrixEventCB) {
currentHtmlNode = htmlWalker.increment();
checkEqual(currentHtmlNode, currentMarkdownNode);
}
outputs.forEach(o => 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<void> {
+23 -11
View File
@@ -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<rawJSX>;
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<TextNode>(NodeTag.TextNode, node, rawChild);
} else {
makeLeafNode<TextNode>(NodeTag.TextNode, node, rawChild);
} else if (typeof rawChild === 'number') {
makeLeafNode<TextNode>(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;
}
}
}