Compare commits

..

4 Commits

Author SHA1 Message Date
0fc074f968 add strikethrough (somewhat broken)
All checks were successful
Build and Deploy Client / build-and-deploy (push) Successful in 9s
2026-09-12 23:47:27 -05:00
fbe9765ef0 cleanup markup code
All checks were successful
Build and Deploy Client / build-and-deploy (push) Successful in 10s
2026-09-12 23:39:34 -05:00
99a4f5c1c8 cleanup and comments
All checks were successful
Build and Deploy Client / build-and-deploy (push) Successful in 8s
2026-08-30 13:28:16 -05:00
b4142781dd semi-live editor
All checks were successful
Build and Deploy Client / build-and-deploy (push) Successful in 9s
2026-08-30 01:01:54 -05:00
3 changed files with 122 additions and 70 deletions

View File

@@ -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>

View File

@@ -3,6 +3,11 @@ p {
font-size: larger;
}
.markup_container {
border: 3px solid #000000;
}
// markup classes
.format-bold {
font-weight: bold;
}
@@ -14,3 +19,7 @@ p {
.format-underline {
text-decoration: underline solid #000000 3px;
}
.format-strikethrough {
text-decoration: line-through;
}

View File

@@ -1,17 +1,24 @@
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;
@@ -19,6 +26,7 @@ interface blockNode {
}
// styles registry: this maps format specifiers to css classes
// might move this to its own file
const styles: Record<string, formatDescriptor> = {
B: {
className: "format-bold"
@@ -29,14 +37,39 @@ const styles: Record<string, formatDescriptor> = {
U: {
className: "format-underline"
},
P: { // for testing. TODO: right now everything is a style
className: "format-plain"
},
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<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({
selector: 'app-markleft',
imports: [],
imports: [FormsModule],
templateUrl: './markleft.html',
styleUrl: './markleft.scss',
})
@@ -45,88 +78,94 @@ 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);
});
this.refresh();
}
// full demo right here
plaintext: string = `
#H{Hello World!}
// 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 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.
`; // TODO: live editor
`;
// 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 | 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) {
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++;
if(input[index] !== "{") { // TODO: implement some expect("{") function to better validate
console.log("{ not found following format specifier");
// return null node
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);
}
console.log("Parsing text for format specifier", code);
let styles: string[] = [];
let styleList: string[] = [];
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 {
styles = [code];
styleList= [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);
}
addTextNode(text, styleList, parentNode);
text = ""; // clear the already commited text
// and then move to the next
parseStyle(parentNode ? parentNode : null, styles); // recursion
parseStyle(parentNode ? parentNode : null, styleList); // recursion
} else if(input[index] == "}") { // this node has closed
addTextNode(text, styles, parentNode);
//index++; // step to next
addTextNode(text, styleList, parentNode);
break;
} 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) {
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++;
if(input[index] !== "{") { // TODO: implement some expect("{") function to better validate
console.log("{ not found following format specifier");
// return null node
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);
}
console.log("Parsing structure for format specifier", code);
// create the node
const node: blockNode = {
@@ -139,17 +178,7 @@ export class Markleft {
while(true) { // assemble text
index++;
if(input[index] === "%" || input[index] === "#") { // encountered another format specifier
// commit current plain text to the block
if(text.length !== 0) {
const textNode: textNode = {
type: "text",
text: text,
styles: [] // none
};
node.children.push(textNode);
}
addTextNode(text, null, node);
text = ""; // clear the already commited text
// and then move to the next
if(input[index] === "%") {
@@ -161,7 +190,6 @@ export class Markleft {
index++;
// add its text
addTextNode(text, null, node);
// push node to children list
if(parentNode) {
parentNode.children.push(node);
@@ -173,6 +201,9 @@ export class Markleft {
} else { // regular text
text += input[index];
}
if(index >= input.length) {
break;
}
}
}
@@ -191,7 +222,6 @@ export class Markleft {
}
function parse(endChar: string | null = null) {
console.log("parsing...");
let currentText: string = "";
while(index < input.length) {
if(endChar && (input[index] === endChar)) {
@@ -207,7 +237,7 @@ export class Markleft {
parseStructure();
currentText = "";
} else if(input[index] === "@") { // encountered an attribute specifier
// nodes.push(parseAttribute());
// nodes.push(parseAttribute()); // TODO: implement
} else {
// regular character
currentText += input[index];
@@ -244,13 +274,7 @@ export class Markleft {
} 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");
}
container = this.renderer.createElement(blocks[node.kind].className);
node.children.forEach((child: textNode | blockNode) => {
if("styles" in child) { // this is a textNode
@@ -277,4 +301,22 @@ export class Markleft {
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);
});
}
}