diff --git a/src/commands/interface-manager/DeadDocument.ts b/src/commands/interface-manager/DeadDocument.ts new file mode 100644 index 00000000..4a20ce63 --- /dev/null +++ b/src/commands/interface-manager/DeadDocument.ts @@ -0,0 +1,147 @@ +/** + * Copyright (C) 2023 Gnuxie + * All rights reserved. + */ + +export interface IDocumentNodeProtoype { + readonly parent: DocumentNode|null; + addText(data: string): TextNode; + addInlineCode(data: string): InlineCodeNode; + readonly leafNode: false; +} + +export interface DocumentNode extends IDocumentNodeProtoype { + addChild(node: Node): Node + getChildren(): (DocumentNode|LeafNode)[] +} + + +// These are NOT HTML tags. +export enum NodeTag { + TextNode = 'text', + InlineCode = 'inline-code', + PreformattedText = 'preformatted-text', +} + +export interface LeafNode { + readonly tag: NodeTag, + readonly parent: DocumentNode, + readonly data: string, + readonly leafNode: true, +} + +export function makeLeafNode(tag: LeafInterface['tag'], parent: DocumentNode, data: string): LeafInterface { + return { tag, parent, data, leafNode: true } as LeafInterface; +} + +export interface TextNode extends LeafNode { + readonly tag: NodeTag.TextNode; +} + +// lol no mixins so addChild can't be protected on DocumentNode +// it's a yoke. +export function addText(this: DocumentNode, data: string): TextNode { + return this.addChild(makeLeafNode(NodeTag.TextNode, this, data)) +} + +export interface InlineCodeNode extends LeafNode { + readonly tag: NodeTag.InlineCode; +} + +export function addInlineCode(this: DocumentNode, data: string): InlineCodeNode { + return this.addChild(makeLeafNode(NodeTag.InlineCode, this, data)); +} + +export interface PreformattedTextNode extends LeafNode { + readonly tag: NodeTag.PreformattedText; +} + +export function addPreformattedText(this: DocumentNode, data: string): PreformattedTextNode { + return this.addChild(makeLeafNode(NodeTag.PreformattedText, this, data)); +} + +function nextSibling(node: DocumentNode|LeafNode): DocumentNode|LeafNode|undefined { + if (node.parent) { + const nodePosition = node.parent.getChildren().indexOf(node); + if (nodePosition === -1) { + throw new TypeError(`Badly constructed parent node for ${node}, apparently this node is not a child.`); + } + return node.parent.getChildren().at(nodePosition + 1); + } + return undefined; +} + + +/** + * We use a Fringe to render. + */ + +export enum FringeType { + Pre = 'pre', + Leaf = 'leaf', + Post = 'post', +} + +type AnnotatedFringeNode = { + type: FringeType, + node: DocumentNode|LeafNode +}; + +type Flat = AnnotatedFringeNode[]; +// https://dl.acm.org/doi/pdf/10.1145/165507.165514 +// If performance becomes a concern then a generator should be written +// but for rendering incrementally i think constructing the fringe is acceptable. +// So long as no one uses `Flat` directly and there's some inversion of control +// then we'll be fine. +function fringeInternalNode(node: DocumentNode, flat: Flat): Flat { + if (node.getChildren().length === 0) { + return flat; + } else { + return node.getChildren().reduce((previous: Flat, child: DocumentNode|LeafNode) => { + return fringe(child, previous); + }, flat); + } +} + +function fringe(node: DocumentNode|LeafNode, flat: Flat = []): Flat { + if (node.leafNode) { + flat.push({ type: FringeType.Leaf, node }); + return flat; + } else { + flat.push({ type: FringeType.Pre, node }); + flat = fringeInternalNode(node, flat); + flat.push({ type: FringeType.Post, node }); + return flat; + } +} +export type FringeLeafRenderFunction = (tag: NodeTag, node:LeafNode, context: Context) => void + +export interface FringeRenderer { + getLeafRenderer(tag: NodeTag): FringeLeafRenderFunction +} + +/** + * The FringeWalker allows for the implementation of an incremental + * renderer. + * Each increment is defined by the fist leaf node to be rendered + * or the first inner node to have all of its leaves renderered. + * @param Context is a static context that should be provided to each render function. + */ +export class FringeWalker { + private readonly fringe: Flat; + constructor( + private readonly root: DocumentNode, + private readonly context: Context, + private readonly renderer: FringeRenderer, + ) { + this.fringe = fringe(root); + } + + public increment(): DocumentNode|LeafNode { + for (const node of this.fringe) { + if (node.type === FringeType.Pre) { + this.renderer.getRenderer(node.node) + } + } + } +} \ No newline at end of file diff --git a/src/commands/interface-manager/DeadDocumentMarkdown.ts b/src/commands/interface-manager/DeadDocumentMarkdown.ts new file mode 100644 index 00000000..2e59c588 --- /dev/null +++ b/src/commands/interface-manager/DeadDocumentMarkdown.ts @@ -0,0 +1,98 @@ +/** + * Copyright (C) 2023 Gnuxie + * All rights reserved. + */ + +import { DocumentNode, NodeTag } from "./DeadDocument"; + +// This just doesn't work. +// the transactions have to be managed by the Matrix renderer +// which is basically a different walker +// that renders the node twice. +// The first thing to get full we stop. +// this is possible if we make an iterator that just renders 1 node at a time +class TransactionalOutputStream { + private buffer: string = ''; + public output: string = ''; + private lastCommittedNode?: DocumentNode; + constructor( + public readonly sizeLimit = 20_000, + ) { + + } + + public writeString(string: string): TransactionalOutputStream { + this.buffer += string; + return this; + } + + public getPosition(): number { + return this.buffer.length; + } + + private adjust(source: string, target: string): void { + if (source.at(-2) === '\n' && source.at(-1) === '\n') return; + if (source.at(-1) === '\n') { + target += '\n'; + return; + } + target += '\n\n'; + } + + public adjustForNextElement(): void { + this.adjust(this.buffer, this.buffer) + } + + public commit(node: DocumentNode): void { + if ((this.output.length + this.buffer.length) > this.sizeLimit) { + throw new Error("Unable to commit blah blah FIXME") + } + this.output += this.buffer; + this.buffer = ''; + this.adjust(this.output, this.buffer); + this.lastCommittedNode = node; + } + + public getLastCommittedNode(): DocumentNode|undefined { + return this.lastCommittedNode; + } +} + +type NodeRenderer = (node: DocumentNode, stream: TransactionalOutputStream) => void; + +const NODE_RENDERERS = new Map(); + +function defineNodeRenderer(tag: NodeTag, renderer: NodeRenderer): NodeRenderer { + if (NODE_RENDERERS.has(tag)) { + throw new TypeError(`There was already a renderer registered for ${tag}`); + } + NODE_RENDERERS.set(tag, render); + return render +} + +function findNodeRenderer(tag: NodeTag): NodeRenderer { + const entry = NODE_RENDERERS.get(tag) + if (entry) { + return entry; + } + throw new TypeError(`Couldn't find a renderer for the tag ${tag}`); +} + +function callNodeRendererForChildren(tag: NodeTag, node: DocumentNode): void { + // The problem is that we want to control message length and split semantically + // so this renderer will not work. +} + +function render(document: DocumentNode): string { + const stream = new TransactionalOutputStream(); + renderDocumentNote(document, stream); + return stream.output; +} + +function renderDocumentNote(node: DocumentNode, stream: TransactionalOutputStream): void { + const previousPosition = stream.getPosition(); + + if (stream.getPosition() !== previousPosition) { + stream.adjustForNextElement(); + } +}