import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; // type descriptions interface formatDescriptor { className: string; }; interface textNode { type: string; text: string; styles: string[] } interface blockNode { type: string; kind: string; children: (blockNode | textNode)[] } // styles registry: this maps format specifiers to css classes const styles: Record = { B: { className: "format-bold" }, I: { className: "format-italic" }, U: { className: "format-underline" }, P: { // for testing. TODO: right now everything is a style className: "format-plain" }, }; // i.e. %B{...} will apply "format-bold" @Component({ selector: 'app-markleft', imports: [], templateUrl: './markleft.html', styleUrl: './markleft.scss', }) export class Markleft { @ViewChild('markup_container', { static: true }) container!: ElementRef; constructor(private renderer: Renderer2) { console.log("component constructor"); } ngOnInit() { // DOM manipulation can't occur in constructor const nodes: (textNode | blockNode)[] = this.parsePlaintext(this.plaintext); console.log(nodes) nodes.forEach((node) => { this.renderNode(node); }); } // full demo right here plaintext: string = ` #H{Hello World!} #P{ This is some markleft text. This is %I{italic} and this is %B{bold}. Some might %I{even be %B{both}}. } This should be plain and unstylized. `; // TODO: live editor parsePlaintext(input: string): (textNode | blockNode)[] { let index: number = 0; let nodes: (textNode | blockNode)[] = []; function parseStyle(parentNode: blockNode | null = null, parentStyles: string[] | null = null) { index++; // step onto specifier code const code: string = input[index]; // TODO: validate if this code is a usable code index++; if(input[index] !== "{") { // TODO: implement some expect("{") function to better validate console.log("{ not found following format specifier"); // return null node } console.log("Parsing text for format specifier", code); let styles: string[] = []; if(parentStyles) { styles = [...parentStyles, code] // append style code to styles list of this node } else { styles = [code]; } let text: string = ""; while(true) { // assemble text index++; if(input[index] === "%") { // encountered another style specifier // commit the current node const node: textNode = { type: "text", text: text, styles: styles }; if(parentNode) { parentNode.children.push(node); } else { nodes.push(node); } text = ""; // clear the already commited text // and then move to the next parseStyle(parentNode ? parentNode : null, styles); // recursion } else if(input[index] == "}") { // this node has closed if(text.length === 0) break; // an empty text block is to be ignored const node: textNode = { type: "text", text: text, styles: styles }; if(parentNode) { parentNode.children.push(node); } else { nodes.push(node); } break; } else { // regular text text += input[index]; } } } function parseStructure(parentNode: blockNode | null = null) { index++; // step onto specifier code const code: string = input[index]; // TODO: validate if this code is a usable code index++; if(input[index] !== "{") { // TODO: implement some expect("{") function to better validate console.log("{ not found following format specifier"); // return null node } console.log("Parsing structure for format specifier", code); let text: string = ""; while(true) { // assemble text index++; if(input[index] === "%" || input[index] === "#") { // encountered another format specifier // create the node const node: blockNode = { type: "block", kind: code, children: [] // this blockNode will get passed to children so they can be added to the parent }; // commit text to the block if(text.length !== 0) { const textNode: textNode = { type: "text", text: text, styles: [] // none }; node.children.push(textNode); } if(parentNode) { parentNode.children.push(node); } else { nodes.push(node); } text = ""; // clear the already commited text // and then move to the next if(input[index] === "%") { parseStyle(node, null); // pass in the current node as the future node's parent } else if(input[index] === "#") { parseStructure(node); } } else if(input[index] == "}") { // this node has closed // create the node const node: blockNode = { type: "block", kind: code, children: [] // this blockNode will get passed to children so they can be added to the parent }; // add its text const textNode: textNode = { type: "text", text: text, styles: [] // none in this case }; node.children.push(textNode); nodes.push(node); break; } else { // regular text text += input[index]; } } } function parse(endChar: string | null = null) { console.log("parsing..."); while(index < input.length) { if(endChar && (input[index] === endChar)) { index++; break; } if(input[index] === "%") { // encountered a style specifier parseStyle(); } else if(input[index] === "#") { // encountered a structure specifier parseStructure(); } else if(input[index] === "@") { // encountered an attribute specifier // nodes.push(parseAttribute()); } else { // regular character index++; } } } parse(); return nodes; } renderNode(node: textNode | blockNode, parentContainer: any = null): void { if("styles" in node) { // this node is a textNode const span = this.renderer.createElement("span"); // create container node.styles.forEach((style: string) => { this.renderer.addClass(span, styles[style].className); }); const text = this.renderer.createText(node.text); // create text this.renderer.appendChild(span, text); // add text to container if(parentContainer) { // this container is a child this.renderer.appendChild(parentContainer, span); } else { // add to the top-level DOM this.renderer.appendChild(this.container.nativeElement, span); } } else if("kind" in node) { // this is a blockNode let container: any; if(node.kind === "H") { // TODO: registry for block types container = this.renderer.createElement("h1"); } else if(node.kind === "P") { container = this.renderer.createElement("p"); } else { console.log("container type not recognized"); } node.children.forEach((child: textNode | blockNode) => { if("styles" in child) { // this is a textNode if(child.styles.length === 0) { // just add text const text = this.renderer.createText(child.text); // create text this.renderer.appendChild(container, text); // add text to container } else { // render styles this.renderNode(child, container); // recursion } } else if("kind" in child) { // this is a block node this.renderNode(child, container); // recursion } }); // add container to DOM if(parentContainer) { // this container is a child this.renderer.appendChild(parentContainer, container); } else { // add to the top-level DOM this.renderer.appendChild(this.container.nativeElement, container); } } return; } }