mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-08-28 23:00:45 +00:00
Tidy up the interface-manager lint & doc.
This commit is contained in:
@@ -1,80 +0,0 @@
|
||||
/**
|
||||
* Copyright (C) 2022 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is modified and is NOT licensed under the Apache License.
|
||||
* This modified file incorperates work from mjolnir
|
||||
* https://github.com/matrix-org/mjolnir
|
||||
* which included the following license notice:
|
||||
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*
|
||||
* However, this file is modified and the modifications in this file
|
||||
* are NOT distributed, contributed, committed, or licensed under the Apache License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* A feature that an application supports.
|
||||
* Used by ApplicationCommands as required feature flags they depend on to function.
|
||||
*/
|
||||
export interface ApplicationFeature {
|
||||
name: string,
|
||||
description: string,
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* These are features that have been defined using `defineApplicationCommand`.
|
||||
* you can access them using `getApplicationFeature`.
|
||||
*/
|
||||
const APPLICATION_FEATURES = new Map<string/*feature name*/, ApplicationFeature>();
|
||||
|
||||
export function defineApplicationFeature(feature: ApplicationFeature): void {
|
||||
if (APPLICATION_FEATURES.has(feature.name)) {
|
||||
throw new TypeError(`Application feature has already been defined ${feature.name}`);
|
||||
}
|
||||
APPLICATION_FEATURES.set(feature.name, feature);
|
||||
}
|
||||
|
||||
export function getApplicationFeature(name: string): ApplicationFeature|undefined {
|
||||
return APPLICATION_FEATURES.get(name);
|
||||
}
|
||||
|
||||
export class ApplicationCommand<ExecutorType extends (...args: any) => Promise<any>> {
|
||||
constructor(
|
||||
public readonly requiredFeatures: ApplicationFeature[],
|
||||
public readonly executor: ExecutorType
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
export function defineApplicationCommand<ExecutorType extends (...args: any) => Promise<any>>(
|
||||
requiredFeatureNames: string[],
|
||||
executor: ExecutorType) {
|
||||
const features = requiredFeatureNames.map(name => {
|
||||
const feature = getApplicationFeature(name);
|
||||
if (feature) {
|
||||
return feature
|
||||
} else {
|
||||
throw new TypeError(`Can't find a feature called ${name}`);
|
||||
}
|
||||
})
|
||||
return new ApplicationCommand<ExecutorType>(features, executor);
|
||||
}
|
||||
|
||||
defineApplicationFeature({
|
||||
name: "synapse admin",
|
||||
description: "Requires that the mjolnir account has Synapse admin"
|
||||
});
|
||||
@@ -54,7 +54,7 @@ const WHITESPACE = [' ', '\r', '\f', '\v', '\n', '\t'];
|
||||
* It doesn't produce an AST because there isn't any syntax that can make a tree
|
||||
* just a list.
|
||||
* This allows commands to be dispatched based on `ReadItem`s and allows
|
||||
* for more efficient (in terms of code) parsing of arguments,
|
||||
* for more efficient (in terms of loc) parsing of arguments,
|
||||
* as I will demonstrate <link here when i've done it>.
|
||||
*
|
||||
* The technique used is somewhat inefficient in terms of resources,
|
||||
|
||||
@@ -5,6 +5,27 @@
|
||||
|
||||
import { SuperCoolStream } from "./CommandReader";
|
||||
|
||||
/**
|
||||
* The DeadDocument started as a universal document object model like Pandoc is.
|
||||
* That kind of task is just too big for me though and someone else should have
|
||||
* done it already. Irregardless, the way this is used is a simple DOM that can
|
||||
* be used to incrementally render both HTML and Markdown.
|
||||
* The reason we need to incrementally render both HTML and Markdown
|
||||
* (which really means serialize, hence `DeadDocument`) is so that
|
||||
* we can ensure in Matrix that both renditions of a node (html + markdown)
|
||||
* are always in the same event and not split across multiple events.
|
||||
* This ensures consistency when someone replies to an event that whichever
|
||||
* format the client uses, the reply will be about the same "thing".
|
||||
* So we have the power to split messages across multiple Matrix events
|
||||
* automatically, without the need for micromanagement.
|
||||
*
|
||||
* While originally we were going to generate this DOM using a custom
|
||||
* internal DSL, we discovered it was possible to use JSX templates with a
|
||||
* custom DOM. You can find our own JSXFactory in `./JSXFactory.ts`.
|
||||
*
|
||||
* This means that end users shouldn't have to touch this DOM directly.
|
||||
*/
|
||||
|
||||
export interface AbstractNode {
|
||||
readonly parent: DocumentNode|null;
|
||||
readonly leafNode: boolean;
|
||||
@@ -102,7 +123,7 @@ export interface InlineCodeNode extends LeafNode {
|
||||
|
||||
export function addInlineCode(this: DocumentNode, data: string): InlineCodeNode {
|
||||
return this.addChild(makeLeafNode<InlineCodeNode>(NodeTag.InlineCode, this, data));
|
||||
}
|
||||
}
|
||||
|
||||
export interface PreformattedTextNode extends LeafNode {
|
||||
readonly tag: NodeTag.PreformattedText;
|
||||
@@ -155,7 +176,7 @@ function fringe(node: DocumentNode|LeafNode, flat: Flat = []): Flat {
|
||||
}
|
||||
}
|
||||
export type FringeLeafRenderFunction<Context> = (tag: NodeTag, node: LeafNode, context: Context) => void
|
||||
export type FringeInnerRenderFunction<Context> = (type: FringeType, node: DocumentNode, context: Context, environment: TagDynamicEnvironment) => void;
|
||||
export type FringeInnerRenderFunction<Context> = (type: FringeType, node: DocumentNode, context: Context, environment: TagDynamicEnvironment) => void;
|
||||
|
||||
export interface FringeRenderer<Context> {
|
||||
getLeafRenderer(tag: NodeTag): FringeLeafRenderFunction<Context>
|
||||
@@ -200,7 +221,7 @@ export class SimpleFringeRenderer<Context> implements FringeRenderer<Context> {
|
||||
|
||||
public registerRenderer<T extends FringeInnerRenderFunction<Context>|FringeLeafRenderFunction<Context>>(type: FringeType, tag: NodeTag, renderer: T): SimpleFringeRenderer<Context> {
|
||||
// The casting in here is evil. Not sure how to fix it.
|
||||
switch(type) {
|
||||
switch (type) {
|
||||
case FringeType.Pre:
|
||||
this.internRenderer<T>(type, tag, this.preRenderers as Map<NodeTag, T>, renderer);
|
||||
break;
|
||||
@@ -273,31 +294,32 @@ export class FringeWalker<Context> {
|
||||
this.dynamicEnvironment.pop(node.node);
|
||||
return node.node;
|
||||
}
|
||||
while(this.stream.peekItem() && !COMMITTABLE_NODES.has(this.stream.peekItem().node.tag)) {
|
||||
const node = this.stream.readItem();
|
||||
switch(node.type) {
|
||||
while (this.stream.peekItem() && !COMMITTABLE_NODES.has(this.stream.peekItem().node.tag)) {
|
||||
const annotatedNode = this.stream.readItem();
|
||||
switch (annotatedNode.type) {
|
||||
case FringeType.Pre:
|
||||
renderInnerNode(node);
|
||||
renderInnerNode(annotatedNode);
|
||||
break;
|
||||
case FringeType.Post:
|
||||
postNode(node);
|
||||
postNode(annotatedNode);
|
||||
break;
|
||||
case FringeType.Leaf:
|
||||
if (node.node.leafNode !== true) {
|
||||
if (annotatedNode.node.leafNode !== true) {
|
||||
throw new TypeError("Leaf nodes should not be marked as an inner node");
|
||||
}
|
||||
this.renderer.getLeafRenderer(node.node.tag)(node.node.tag, node.node as unknown as LeafNode, this.context);
|
||||
this.renderer.getLeafRenderer(annotatedNode.node.tag)
|
||||
(annotatedNode.node.tag, annotatedNode.node as unknown as LeafNode, this.context);
|
||||
break;
|
||||
default:
|
||||
throw new TypeError(`Uknown fringe type ${node.type}`);
|
||||
throw new TypeError(`Uknown fringe type ${annotatedNode.type}`);
|
||||
}
|
||||
}
|
||||
if (this.stream.peekItem() === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
const node = postNode(this.stream.readItem());
|
||||
this.commitHook(node, this.context);
|
||||
return node;
|
||||
const documentNode = postNode(this.stream.readItem());
|
||||
this.commitHook(documentNode, this.context);
|
||||
return documentNode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,11 +333,24 @@ export class 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
|
||||
/**
|
||||
* A dynamic environment is just an environment of bindings that is made
|
||||
* by shadowing previous bindings and pushing and popping bindings "dynamically"
|
||||
* for a given variable with some thing.
|
||||
*
|
||||
* In this example, we push and pop bindings with `DocumentNode`s.
|
||||
* For example, if you make a binding to a variable called `indentationLevel`
|
||||
* to set it to `1` from a `<ul>` node, then this binding should be popped
|
||||
* when the `FringeWalker` reaches the post node (`</ul>`).
|
||||
* Howerver, if we encounter another `<ul>`, we can read the existing value
|
||||
* for `indentationLevel`, increment it and create a new binding
|
||||
* that shadows the existing one. This too will get popped once we encounter
|
||||
* the post node for this `<ul>` node (`</ul>`).
|
||||
*
|
||||
* This makes it very easy to express a situation where you modify and
|
||||
* restore variables that depend on node depth when walking the fringe,
|
||||
* as the restoration of previous values can be handled automatically for us.
|
||||
*/
|
||||
export class TagDynamicEnvironment {
|
||||
private readonly environments = new Map<string, TagDynamicEnvironmentEntry|undefined>();
|
||||
|
||||
|
||||
@@ -4,101 +4,9 @@
|
||||
*/
|
||||
|
||||
import { DocumentNode, FringeInnerRenderFunction, FringeLeafRenderFunction, FringeType, LeafNode, NodeTag, SimpleFringeRenderer, TagDynamicEnvironment } from "./DeadDocument";
|
||||
import { PagedDuplexStream } from "./PagedDuplexStream";
|
||||
|
||||
/**
|
||||
* Ideally this would call a callback when a page is ready
|
||||
* Unfortunatley there's no way to do that (and await) without making the stream
|
||||
* all async. Which is annoying af.
|
||||
* Therefore it's necessary for the stream to queue pages
|
||||
*/
|
||||
export class PagedDuplexStream {
|
||||
private buffer: string = '';
|
||||
private pages: string[] = [''];
|
||||
|
||||
private lastCommittedNode?: DocumentNode;
|
||||
constructor(
|
||||
public readonly sizeLimit = 20_000,
|
||||
) {
|
||||
}
|
||||
|
||||
private get currentPage(): string {
|
||||
return this.pages.at(this.pages.length - 1)!;
|
||||
}
|
||||
|
||||
private appendToCurrentPage(string: string) {
|
||||
const currentIndex = this.pages.length - 1;
|
||||
this.pages[currentIndex] = this.pages[currentIndex] + string;
|
||||
}
|
||||
|
||||
public writeString(string: string): PagedDuplexStream {
|
||||
this.buffer += string;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPosition(): number {
|
||||
return this.buffer.length;
|
||||
}
|
||||
|
||||
public isPageAndBufferOverSize(): boolean {
|
||||
return (this.currentPage.length + this.buffer.length) > this.sizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new page from the previously committed text
|
||||
* @returns A page with all committed text.
|
||||
*/
|
||||
public ensureNewPage(): void {
|
||||
if (this.currentPage.length !== 0) {
|
||||
this.pages.push('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the buffered text to the current page.
|
||||
* If the buffered text is over the `sizeLimit`, then the current
|
||||
* page will be returned first, and then replaced with a new one in order
|
||||
* to commit the buffer.
|
||||
* @param node A DocumentNode to associate with the commit.
|
||||
* @throws TypeError if the buffer is larger than the `sizeLimit`.
|
||||
* @returns A page if the buffered text will force the current page to go over the size limit.
|
||||
*/
|
||||
public commit(node: DocumentNode): void {
|
||||
if (this.isPageAndBufferOverSize()) {
|
||||
if (this.currentPage.length === 0 && (this.buffer.length > this.sizeLimit)) {
|
||||
throw new TypeError('Commit is too large, could not write a page for this commit');
|
||||
}
|
||||
this.ensureNewPage();
|
||||
this.appendToCurrentPage(this.buffer);
|
||||
this.lastCommittedNode = node;
|
||||
} else {
|
||||
this.appendToCurrentPage(this.buffer);
|
||||
this.buffer = '';
|
||||
this.lastCommittedNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastCommittedNode(): DocumentNode|undefined {
|
||||
return this.lastCommittedNode;
|
||||
}
|
||||
|
||||
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 {
|
||||
export enum MarkdownVariables {
|
||||
IndentationLevel = "indentation level"
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
import { MatrixSendClient } from "../../MatrixEmitter";
|
||||
import { AbstractNode, DocumentNode, FringeWalker } from "./DeadDocument";
|
||||
import { HTML_RENDERER } from "./DeadDocumentHtml";
|
||||
import { MARKDOWN_RENDERER, PagedDuplexStream } from "./DeadDocumentMarkdown";
|
||||
import { MARKDOWN_RENDERER } from "./DeadDocumentMarkdown";
|
||||
import { PagedDuplexStream } from "./PagedDuplexStream";
|
||||
|
||||
function checkEqual(node1: AbstractNode|undefined, node2: AbstractNode|undefined): true {
|
||||
if (!Object.is(node1, node2)) {
|
||||
@@ -17,9 +18,17 @@ function checkEqual(node1: AbstractNode|undefined, node2: AbstractNode|undefined
|
||||
|
||||
export type SendMatrixEventCB = (text: string, html: string) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Render the `DocumentNode` to Matrix (in both HTML + Markdown) using the
|
||||
* callback provided to send each event. Should serialized content span
|
||||
* more than one event, then the callback will be called for each event.
|
||||
* @param node A document node to render to Matrix.
|
||||
* @param cb A callback that will send the text+html for a single event
|
||||
* to a Matrix room.
|
||||
*/
|
||||
export async function renderMatrix(node: DocumentNode, cb: SendMatrixEventCB) {
|
||||
const commitHook = (node: DocumentNode, context: { output: PagedDuplexStream }) => {
|
||||
context.output.commit(node);
|
||||
const commitHook = (commitNode: DocumentNode, context: { output: PagedDuplexStream }) => {
|
||||
context.output.commit(commitNode);
|
||||
};
|
||||
const markdownOutput = new PagedDuplexStream();
|
||||
const markdownWalker = new FringeWalker(
|
||||
@@ -60,6 +69,13 @@ export async function renderMatrix(node: DocumentNode, cb: SendMatrixEventCB) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the document node to html+text `m.notice` events.
|
||||
* @param node The document node to render.
|
||||
* @param roomId The room to send the events to.
|
||||
* @param event An event to reply to.
|
||||
* @param client A MatrixClient to send the events with.
|
||||
*/
|
||||
export async function renderMatrixAndSend(node: DocumentNode, roomId: string, event: any, client: MatrixSendClient): Promise<void> {
|
||||
// We desperatley need support for threads to make this work in a non-shit way.
|
||||
await renderMatrix(node, async (text: string, html: string) => {
|
||||
|
||||
@@ -16,7 +16,7 @@ export function JSXFactory(tag: NodeTag, properties: any, ...rawChildren: (Docum
|
||||
makeLeafNode<TextNode>(NodeTag.TextNode, node, (rawChild as number).toString());
|
||||
} else if (Array.isArray(rawChild)) {
|
||||
rawChild.forEach(ensureChild);
|
||||
} else if (typeof rawChild.leafNode === 'boolean'){
|
||||
} else if (typeof rawChild.leafNode === 'boolean') {
|
||||
node.addChild(rawChild);
|
||||
} else {
|
||||
throw new TypeError(`Unexpected raw child ${JSON.stringify(rawChild)}`)
|
||||
@@ -30,7 +30,7 @@ export function JSXFactory(tag: NodeTag, properties: any, ...rawChildren: (Docum
|
||||
namespace JSXFactory {
|
||||
export interface IntrinsicElements {
|
||||
[elemName: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -42,7 +42,7 @@ declare global {
|
||||
export namespace JSX {
|
||||
export interface IntrinsicElements {
|
||||
[elemName: string]: any;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
|
||||
* All rights reserved.
|
||||
*/
|
||||
|
||||
import { DocumentNode } from "./DeadDocument";
|
||||
|
||||
/**
|
||||
* Ideally this would call a callback when a page is ready
|
||||
* Unfortunatley there's no way to do that (and await) without making the stream
|
||||
* all async. Which is annoying af.
|
||||
* Therefore it's necessary for the stream to queue pages
|
||||
*/
|
||||
|
||||
export class PagedDuplexStream {
|
||||
private buffer = '';
|
||||
private pages: string[] = [''];
|
||||
|
||||
private lastCommittedNode?: DocumentNode;
|
||||
constructor(
|
||||
public readonly sizeLimit = 20000
|
||||
) {
|
||||
}
|
||||
|
||||
private get currentPage(): string {
|
||||
return this.pages.at(this.pages.length - 1)!;
|
||||
}
|
||||
|
||||
private appendToCurrentPage(string: string) {
|
||||
const currentIndex = this.pages.length - 1;
|
||||
this.pages[currentIndex] = this.pages[currentIndex] + string;
|
||||
}
|
||||
|
||||
public writeString(string: string): PagedDuplexStream {
|
||||
this.buffer += string;
|
||||
return this;
|
||||
}
|
||||
|
||||
public getPosition(): number {
|
||||
return this.buffer.length;
|
||||
}
|
||||
|
||||
public isPageAndBufferOverSize(): boolean {
|
||||
return (this.currentPage.length + this.buffer.length) > this.sizeLimit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new page from the previously committed text
|
||||
* @returns A page with all committed text.
|
||||
*/
|
||||
public ensureNewPage(): void {
|
||||
if (this.currentPage.length !== 0) {
|
||||
this.pages.push('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the buffered text to the current page.
|
||||
* If the buffered text is over the `sizeLimit`, then the current
|
||||
* page will be returned first, and then replaced with a new one in order
|
||||
* to commit the buffer.
|
||||
* @param node A DocumentNode to associate with the commit.
|
||||
* @throws TypeError if the buffer is larger than the `sizeLimit`.
|
||||
* @returns A page if the buffered text will force the current page to go over the size limit.
|
||||
*/
|
||||
public commit(node: DocumentNode): void {
|
||||
if (this.isPageAndBufferOverSize()) {
|
||||
if (this.currentPage.length === 0 && (this.buffer.length > this.sizeLimit)) {
|
||||
throw new TypeError('Commit is too large, could not write a page for this commit');
|
||||
}
|
||||
this.ensureNewPage();
|
||||
this.appendToCurrentPage(this.buffer);
|
||||
this.lastCommittedNode = node;
|
||||
} else {
|
||||
this.appendToCurrentPage(this.buffer);
|
||||
this.buffer = '';
|
||||
this.lastCommittedNode = node;
|
||||
}
|
||||
}
|
||||
|
||||
public getLastCommittedNode(): DocumentNode | undefined {
|
||||
return this.lastCommittedNode;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user