mirror of
https://github.com/the-draupnir-project/Draupnir.git
synced 2026-10-05 08:57:56 +00:00
Pandoc AST please.
I've just realised my node model is wrong while halfway through writing an incremental FringeWalker to help with renderering events.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
|
||||
* 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 extends DocumentNode|LeafNode>(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<LeafInterface extends LeafNode>(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<TextNode>(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<InlineCodeNode>(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<PreformattedTextNode>(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<Context> = (tag: NodeTag, node:LeafNode, context: Context) => void
|
||||
|
||||
export interface FringeRenderer<Context> {
|
||||
getLeafRenderer(tag: NodeTag): FringeLeafRenderFunction<Context>
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<Context> {
|
||||
private readonly fringe: Flat;
|
||||
constructor(
|
||||
private readonly root: DocumentNode,
|
||||
private readonly context: Context,
|
||||
private readonly renderer: FringeRenderer<Context>,
|
||||
) {
|
||||
this.fringe = fringe(root);
|
||||
}
|
||||
|
||||
public increment(): DocumentNode|LeafNode {
|
||||
for (const node of this.fringe) {
|
||||
if (node.type === FringeType.Pre) {
|
||||
this.renderer.getRenderer(node.node)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
/**
|
||||
* Copyright (C) 2023 Gnuxie <Gnuxie@protonmail.com>
|
||||
* 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<NodeTag, NodeRenderer>();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user