Compare commits
6 Commits
ec40481db8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fc074f968 | |||
| fbe9765ef0 | |||
| 99a4f5c1c8 | |||
| b4142781dd | |||
| 039dc4de0c | |||
| 95fe7c7745 |
@@ -1,3 +1,4 @@
|
|||||||
<p>markleft works!</p>
|
|
||||||
|
|
||||||
<div #markup_container></div>
|
<textarea [(ngModel)]="liveText" (ngModelChange)="onTextUpdate($event)"></textarea>
|
||||||
|
|
||||||
|
<div #markup_container class="markup_container"></div>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
|
||||||
.format-plain {
|
p {
|
||||||
font-weight: normal;
|
font-size: larger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.markup_container {
|
||||||
|
border: 3px solid #000000;
|
||||||
|
}
|
||||||
|
|
||||||
|
// markup classes
|
||||||
.format-bold {
|
.format-bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
@@ -14,3 +19,7 @@
|
|||||||
.format-underline {
|
.format-underline {
|
||||||
text-decoration: underline solid #000000 3px;
|
text-decoration: underline solid #000000 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.format-strikethrough {
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core';
|
import { Component, ElementRef, Renderer2, ViewChild } from '@angular/core';
|
||||||
|
import { FormsModule } from '@angular/forms'
|
||||||
|
|
||||||
// type descriptions
|
// type descriptions
|
||||||
|
|
||||||
|
// a formatDescriptor contains information for the renderer to apply formatting styles to a node
|
||||||
interface formatDescriptor {
|
interface formatDescriptor {
|
||||||
className: string;
|
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 {
|
interface textNode {
|
||||||
type: string;
|
type: string;
|
||||||
text: string;
|
text: string;
|
||||||
styles: 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 {
|
interface blockNode {
|
||||||
type: string;
|
type: string;
|
||||||
kind: string;
|
kind: string;
|
||||||
@@ -19,6 +26,7 @@ interface blockNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// styles registry: this maps format specifiers to css classes
|
// styles registry: this maps format specifiers to css classes
|
||||||
|
// might move this to its own file
|
||||||
const styles: Record<string, formatDescriptor> = {
|
const styles: Record<string, formatDescriptor> = {
|
||||||
B: {
|
B: {
|
||||||
className: "format-bold"
|
className: "format-bold"
|
||||||
@@ -29,14 +37,39 @@ const styles: Record<string, formatDescriptor> = {
|
|||||||
U: {
|
U: {
|
||||||
className: "format-underline"
|
className: "format-underline"
|
||||||
},
|
},
|
||||||
P: { // for testing. TODO: right now everything is a style
|
S: {
|
||||||
className: "format-plain"
|
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"
|
}; // 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<string, formatDescriptor> = {
|
||||||
|
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({
|
@Component({
|
||||||
selector: 'app-markleft',
|
selector: 'app-markleft',
|
||||||
imports: [],
|
imports: [FormsModule],
|
||||||
templateUrl: './markleft.html',
|
templateUrl: './markleft.html',
|
||||||
styleUrl: './markleft.scss',
|
styleUrl: './markleft.scss',
|
||||||
})
|
})
|
||||||
@@ -45,175 +78,245 @@ export class Markleft {
|
|||||||
@ViewChild('markup_container', { static: true }) container!: ElementRef;
|
@ViewChild('markup_container', { static: true }) container!: ElementRef;
|
||||||
|
|
||||||
constructor(private renderer: Renderer2) {
|
constructor(private renderer: Renderer2) {
|
||||||
console.log("component constructor");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() { // DOM manipulation can't occur in constructor
|
ngOnInit() { // DOM manipulation can't occur in constructor
|
||||||
|
this.refresh();
|
||||||
const nodes: textNode[] = this.parsePlaintext("%P{this is a %B{test.} This should be unbolded. But %I{this} should be italicized.}");
|
|
||||||
console.log(nodes);
|
|
||||||
nodes.forEach((node) => {
|
|
||||||
this.renderNode(node);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// full demo right here
|
// live update on each text input.
|
||||||
plaintext: String = `
|
onTextUpdate(event: Event) {
|
||||||
|
this.refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// default text
|
||||||
|
liveText: string = `
|
||||||
#H1{Hello World!}
|
#H1{Hello World!}
|
||||||
|
#H2{hi mom !!}
|
||||||
#P{
|
#P{
|
||||||
This is some markleft text. This is %I{italic} and this is %B{bold}. Some might %I{even be %B{both}}.
|
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.
|
This should be plain and unstylized.
|
||||||
`; // TODO: live editor
|
`;
|
||||||
|
|
||||||
parsePlaintext(input: string): textNode[] {
|
// 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;
|
let index: number = 0; // position in the input string
|
||||||
let nodes: textNode[] = [];
|
let nodes: (textNode | blockNode)[] = [];
|
||||||
|
|
||||||
// starting with ONLY text styles, no structure blocks yet
|
// 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(parentStyles: string | null = null) {
|
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
|
// step onto format specifier code
|
||||||
index++;
|
index++;
|
||||||
if(input[index] !== "{") { // TODO: implement some expect("{") function to better validate
|
let codeIndexStart: number = index;
|
||||||
console.log("{ not found following format specifier");
|
while(input[index] != "{") { // continue stepping forward until we hit a { character
|
||||||
// return null node
|
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);
|
||||||
}
|
}
|
||||||
console.log("Parsing text for format specifier", code);
|
|
||||||
|
|
||||||
let styles: string[] = [];
|
let styleList: string[] = [];
|
||||||
if(parentStyles) {
|
if(parentStyles) {
|
||||||
styles = [...parentStyles, code] // append style code to styles list of this node
|
styleList = [...parentStyles, code] // append style code to styles list of this node
|
||||||
} else {
|
} else {
|
||||||
styles = [code];
|
styleList= [code];
|
||||||
}
|
}
|
||||||
|
|
||||||
let text: string = "";
|
let text: string = "";
|
||||||
while(true) { // assemble text
|
while(true) { // assemble text
|
||||||
index++;
|
index++;
|
||||||
console.log(input[index])
|
|
||||||
if(input[index] === "%") { // encountered another style specifier
|
if(input[index] === "%") { // encountered another style specifier
|
||||||
|
|
||||||
// commit the current node
|
// commit the current node
|
||||||
const node: textNode = {
|
addTextNode(text, styleList, parentNode);
|
||||||
type: "text",
|
|
||||||
text: text,
|
|
||||||
styles: styles
|
|
||||||
};
|
|
||||||
nodes.push(node);
|
|
||||||
text = ""; // clear the already commited text
|
text = ""; // clear the already commited text
|
||||||
// and then move to the next
|
// and then move to the next
|
||||||
parseStyle(); // recursion
|
parseStyle(parentNode ? parentNode : null, styleList); // recursion
|
||||||
|
|
||||||
} else if(input[index] == "}") { // this node has closed
|
} else if(input[index] == "}") { // this node has closed
|
||||||
const node: textNode = {
|
addTextNode(text, styleList, parentNode);
|
||||||
type: "text",
|
|
||||||
text: text,
|
|
||||||
styles: styles
|
|
||||||
};
|
|
||||||
nodes.push(node);
|
|
||||||
break;
|
break;
|
||||||
} else { // regular text
|
} else { // regular text
|
||||||
text += input[index];
|
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) {
|
function parse(endChar: string | null = null) {
|
||||||
console.log("parsing...");
|
let currentText: string = "";
|
||||||
while(index < input.length) {
|
while(index < input.length) {
|
||||||
if(endChar && (input[index] === endChar)) {
|
if(endChar && (input[index] === endChar)) {
|
||||||
index++;
|
index++;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if(input[index] === "%") { // encountered a style specifier
|
if(input[index] === "%") { // encountered a style specifier
|
||||||
|
addTextNode(currentText);
|
||||||
parseStyle();
|
parseStyle();
|
||||||
|
currentText = "";
|
||||||
} else if(input[index] === "#") { // encountered a structure specifier
|
} else if(input[index] === "#") { // encountered a structure specifier
|
||||||
// nodes.push(parseStructure());
|
addTextNode(currentText);
|
||||||
|
parseStructure();
|
||||||
|
currentText = "";
|
||||||
} else if(input[index] === "@") { // encountered an attribute specifier
|
} else if(input[index] === "@") { // encountered an attribute specifier
|
||||||
// nodes.push(parseAttribute());
|
// nodes.push(parseAttribute()); // TODO: implement
|
||||||
} else {
|
} else {
|
||||||
// regular character
|
// regular character
|
||||||
|
currentText += input[index];
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
addTextNode(currentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* pseudocode (c lol)
|
|
||||||
|
|
||||||
int index = 0;
|
|
||||||
|
|
||||||
void parse(char endChar = null, formatDescriptor[]) {
|
|
||||||
const nodes = []; // start with an empty list of nodes
|
|
||||||
|
|
||||||
while(index < input.length) { // iterate over the input string
|
|
||||||
// break if } encountered, step forward (this format block has exited)
|
|
||||||
if(endChar && (input[pos] == endChar)) {
|
|
||||||
index++;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(input[index] == "%") {
|
|
||||||
nodes.push(parseStyle(...));
|
|
||||||
} else if(input[index] == "#") {
|
|
||||||
nodes.push(parseBlock(...));
|
|
||||||
} else {
|
|
||||||
nodes.push(parseText(...));
|
|
||||||
}
|
|
||||||
|
|
||||||
return mergeAdjacentText(nodes);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
void parseStyle(parentStyle) {
|
|
||||||
index++; // step onto specifier
|
|
||||||
char code = readIdentifier(input[index])
|
|
||||||
index++;
|
|
||||||
expect("{"); // parsing error if not
|
|
||||||
const styles = [...parentStyles, code] // append style code to styles list of this node
|
|
||||||
const children = parseUntil("}", styles); // recursion probably
|
|
||||||
|
|
||||||
return children
|
|
||||||
}
|
|
||||||
... and so on for parseBlock and parseText
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
parse();
|
parse();
|
||||||
|
|
||||||
return nodes;
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderNode(node: textNode): void {
|
renderNode(node: textNode | blockNode, parentContainer: any = null): void {
|
||||||
|
|
||||||
console.log("rendering...");
|
if("styles" in node) { // this node is a textNode
|
||||||
|
|
||||||
if(node.type == "text") { // text nodes only need span elements
|
|
||||||
|
|
||||||
const span = this.renderer.createElement("span"); // create container
|
const span = this.renderer.createElement("span"); // create container
|
||||||
|
|
||||||
node.styles.forEach((style) => {
|
node.styles.forEach((style: string) => {
|
||||||
this.renderer.addClass(span, styles[style].className);
|
this.renderer.addClass(span, styles[style].className);
|
||||||
});
|
});
|
||||||
|
|
||||||
const text = this.renderer.createText(node.text); // create text
|
const text = this.renderer.createText(node.text); // create text
|
||||||
|
|
||||||
this.renderer.appendChild(span, text); // add text to container
|
this.renderer.appendChild(span, text); // add text to container
|
||||||
this.renderer.appendChild(this.container.nativeElement, span); // add container to DOM
|
if(parentContainer) { // this container is a child
|
||||||
|
this.renderer.appendChild(parentContainer, span);
|
||||||
} /* else if(node.type == "block") {
|
} else { // add to the top-level DOM
|
||||||
|
this.renderer.appendChild(this.container.nativeElement, span);
|
||||||
element block = html.createElement(node.kind); // or whatever it is
|
|
||||||
|
|
||||||
// recursion
|
|
||||||
foreach(nodeType child in node.children) {
|
|
||||||
block.children.add(renderNode(child));
|
|
||||||
}
|
}
|
||||||
return block;
|
|
||||||
|
|
||||||
} */
|
} 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;
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user