import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core'; import { FormsModule } from '@angular/forms' // type descriptions // a formatDescriptor contains information for the renderer to apply formatting styles to a node interface formatDescriptor { className: string; }; // a textNode is an element produced by the parser to link text to applied format specifiers. The renderer will // apply each style to the text in the node according to the styles registry. interface textNode { type: string; text: string; styles: string[] } // a blockNode is an element produced by the parser to structure nodes by blocks. blocks contain child nodes, either // other blocks or text nodes at the bottom. A block node's html element type is described by kind: p, h1, h2, div, etc // Maybe we blocks can also have styles, and then we can join textNode and blockNode into a single type. interface blockNode { type: string; kind: string; children: (blockNode | textNode)[] } // styles registry: this maps format specifiers to css classes // might move this to its own file const styles: Record = { B: { className: "format-bold" }, I: { className: "format-italic" }, U: { className: "format-underline" }, S: { className: "format-strikethrough" } // TODO: some css style elements cannot simply be overlapped. strikethrough and underline both use the // "text decoration" tag so simply applying both will overwrite one of the styles. some extra logic will have to // be put in place to add a combined "format-strikethrough-formatunderline" style in which the text decoration // contains both. }; // i.e. %B{...} will apply "format-bold" // blocks registry: this maps format specifiers to html block types // if moved to another file, should stick next to the styles registry const blocks: Record = { H1: { className: "h1" }, H2: { className: "h2" }, H3: { className: "h3" }, P: { className: "p" }, D: { className: "div" } // TODO: lists }; // i.e. #H1{...} will create a h1 element // angular component class @Component({ selector: 'app-markleft', imports: [FormsModule], templateUrl: './markleft.html', styleUrl: './markleft.scss', }) export class Markleft { @ViewChild('markup_container', { static: true }) container!: ElementRef; constructor(private renderer: Renderer2) { } ngOnInit() { // DOM manipulation can't occur in constructor this.refresh(); } // live update on each text input. onTextUpdate(event: Event) { this.refresh(); } // default text liveText: string = ` #H1{Hello World!} #H2{hi mom !!} #P{ This is some %B{PhAML} (%I{%B{Ph}ishly %B{A}mazing %B{M}arkup %B{L}anguage}) text. This is %I{italic} and this is %B{bold}. Some might %I{even be %B{both}}. } This should be plain and unstylized. `; // parses a single string into an array of nodes // like noted at parseStyle, maybe can return a single node which itself contains the array of children. parsePlaintext(input: string): (textNode | blockNode)[] { let index: number = 0; // position in the input string let nodes: (textNode | blockNode)[] = []; // TODO: I might restructure the node parsing so that it starts with a generic top level node and can process all children under that node function parseStyle(parentNode: blockNode | null = null, parentStyles: string[] | null = null) { // step onto format specifier code index++; let codeIndexStart: number = index; while(input[index] != "{") { // continue stepping forward until we hit a { character index++; if(index >= input.length) break; } // the resulting substring between the style character and the { is our style code const code: string = input.substring(codeIndexStart, index); if(!(code in styles)) { console.log("Style identifier is not recognized: ", code); } let styleList: string[] = []; if(parentStyles) { styleList = [...parentStyles, code] // append style code to styles list of this node } else { styleList= [code]; } let text: string = ""; while(true) { // assemble text index++; if(input[index] === "%") { // encountered another style specifier // commit the current node addTextNode(text, styleList, parentNode); text = ""; // clear the already commited text // and then move to the next parseStyle(parentNode ? parentNode : null, styleList); // recursion } else if(input[index] == "}") { // this node has closed addTextNode(text, styleList, parentNode); break; } else { // regular text text += input[index]; // append character to the text string } if(index >= input.length) { break; } } } function parseStructure(parentNode: blockNode | null = null) { // step onto format specifier code index++; let codeIndexStart: number = index; while(input[index] != "{") { // continue stepping forward until we hit a { character index++; if(index >= input.length) break; } // the resulting substring between the style character and the { is our style code const code: string = input.substring(codeIndexStart, index); if(!(code in blocks)) { console.log("Code is not recognized: ", code); } // 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 }; let text: string = ""; while(true) { // assemble text index++; if(input[index] === "%" || input[index] === "#") { // encountered another format specifier addTextNode(text, null, 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 index++; // add its text addTextNode(text, null, node); // push node to children list if(parentNode) { parentNode.children.push(node); } else { nodes.push(node); } break; } else { // regular text text += input[index]; } if(index >= input.length) { break; } } } function addTextNode(text: string, styles: string[] | null = null, parentNode: blockNode | null = null) { if(text.length === 0) return; const textNode: textNode = { type: "text", text: text, styles: styles ? styles : [] } if(parentNode) { parentNode.children.push(textNode); } else { nodes.push(textNode); } } function parse(endChar: string | null = null) { let currentText: string = ""; while(index < input.length) { if(endChar && (input[index] === endChar)) { index++; break; } if(input[index] === "%") { // encountered a style specifier addTextNode(currentText); parseStyle(); currentText = ""; } else if(input[index] === "#") { // encountered a structure specifier addTextNode(currentText); parseStructure(); currentText = ""; } else if(input[index] === "@") { // encountered an attribute specifier // nodes.push(parseAttribute()); // TODO: implement } else { // regular character currentText += input[index]; index++; } } addTextNode(currentText); } 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; container = this.renderer.createElement(blocks[node.kind].className); 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; } refresh() { // clear children const parent = this.container.nativeElement; while (parent.firstChild) { this.renderer.removeChild(parent, parent.firstChild); } // parse text const nodes: (textNode | blockNode)[] = this.parsePlaintext(this.liveText); // render nodes nodes.forEach((node) => { this.renderNode(node); }); } }