mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-10-22 17:11:17 +00:00
We can now render to Matrix using JSX.
This commit is contained in:
1 parent
4b7e6637fa
commit
0be83ae8ce
6 files changed
+201
-39
No files matched your search
@@ -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
|
||||
})
|
||||
@@ -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> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user