279 lines
9.9 KiB
TypeScript
279 lines
9.9 KiB
TypeScript
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<string, formatDescriptor> = {
|
|
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;
|
|
}
|
|
}
|