Tidy up the interface-manager lint & doc.

This commit is contained in:
gnuxie
2023-01-14 18:33:06 +00:00
parent 0be83ae8ce
commit 052ded0f30
7 changed files with 179 additions and 200 deletions
@@ -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,
+54 -19
View File
@@ -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) => {
+3 -3
View File
@@ -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();
}
}