diff --git a/components/ttcmd/index.tsx b/components/ttcmd/index.tsx index 8075743..f094a86 100644 --- a/components/ttcmd/index.tsx +++ b/components/ttcmd/index.tsx @@ -2,7 +2,7 @@ import { Accordion, AccordionContent } from "@/lib/accordion"; import { Poppable } from "@/lib/poppables/components/poppable"; -import { buildAbstractSyntaxTree, createElements } from "@/lib/tcmd"; +import { createElements } from "@/lib/tcmd"; import Link from "next/link"; import React, { FC, Fragment, ReactNode, use, useMemo } from "react"; @@ -24,7 +24,7 @@ export const TTCMD: FC<{ body: Promise }> = ({ body }) => { {/* {elements.map((e, i) => {render(e)})} */} - {renderer(elements, tabSpacing)} + {renderer(elements.map((e) => e.token!), tabSpacing)} //
// {/*
{JSON.stringify(elements,null,2)}
*/} @@ -35,7 +35,7 @@ export const TTCMD: FC<{ body: Promise }> = ({ body }) => { const renderer = (tokens: Token[], tabSpacing: number) => { const usedIds: string[] = []; return tokens.map((t) => ( - {render(t, usedIds, tabSpacing)} +
{render(t, usedIds, tabSpacing)}
)); }; @@ -51,7 +51,6 @@ const render = (token: Token, usedIds: string[], tabSpacing: number) => { data-[strength="1"]:text-4xl data-[strength="2"]:text-3xl data-[strength="3"]:text-2xl - p `} > {token.content} @@ -163,6 +162,18 @@ const render = (token: Token, usedIds: string[], tabSpacing: number) => {
); + case "bold": + return ( + + {token.content} + + ); + case "italic": + return ( + + {token.content} + + ); default: return (
@@ -172,90 +183,90 @@ const render = (token: Token, usedIds: string[], tabSpacing: number) => { } }; -const renderBlock = ( - block: BlockChildren, - usedIds: string[] = [], -): ReactNode => { - usedIds = usedIds || []; - switch (block.type) { - case "block": - return block.children.map((e, i) => ( - {renderBlock(e, usedIds)} - )); - case "grid": - return ( -
- {block.children.map((c, i) => ( -
- {renderBlock(c, usedIds)} -
- ))} -
- ); - case "card": - return ( -
- {block.children.map((e, i) => ( - - {renderBlock(e, usedIds)} - - ))} -
- ); - case "accordion": - return ( -
- - - {block.children.map((e, i) => ( - - {renderBlock(e, usedIds)} - - ))} - - -
- ); - default: - return ( - renderParagraph(block as ParagraphToken, usedIds) - ); - } -}; +// const renderBlock = ( +// block: BlockChildren, +// usedIds: string[] = [], +// ): ReactNode => { +// usedIds = usedIds || []; +// switch (block.type) { +// case "block": +// return block.children.map((e, i) => ( +// {renderBlock(e, usedIds)} +// )); +// case "grid": +// return ( +//
+// {block.children.map((c, i) => ( +//
+// {renderBlock(c, usedIds)} +//
+// ))} +//
+// ); +// case "card": +// return ( +//
+// {block.children.map((e, i) => ( +// +// {renderBlock(e, usedIds)} +// +// ))} +//
+// ); +// case "accordion": +// return ( +//
+// +// +// {block.children.map((e, i) => ( +// +// {renderBlock(e, usedIds)} +// +// ))} +// +// +//
+// ); +// default: +// return ( +// renderParagraph(block as ParagraphToken, usedIds) +// ); +// } +// }; -const renderParagraph = (p: ParagraphToken, usedIds: string[]) => { - switch (p.type) { - case "p": - return ( -
- {p.content.map((e, i) => ( - - {renderToken(e, usedIds)} - - ))} -
- ); - case "code": - return ( -
-          {p.content.map((c) => c.line.toString()).join("\n\n")}
-        
- ); - default: - return ( -
- Block or paragraph missing implementation: {p.type} -
- ); - } -}; +// const renderParagraph = (p: ParagraphToken, usedIds: string[]) => { +// switch (p.type) { +// case "p": +// return ( +//
+// {p.content.map((e, i) => ( +// +// {renderToken(e, usedIds)} +// +// ))} +//
+// ); +// case "code": +// return ( +//
+//           {p.content.map((c) => c.line.toString()).join("\n\n")}
+//         
+// ); +// default: +// return ( +//
+// Block or paragraph missing implementation: {p.type} +//
+// ); +// } +// }; const generateId = (t: string, usedIds: string[]) => { let id = t.toLowerCase().replace(/[^a-z\s]/ig, "").trim().replaceAll( @@ -271,136 +282,136 @@ const generateId = (t: string, usedIds: string[]) => { return id; }; -const renderToken = (t: Token, usedIds: string[]) => { - switch (t.type) { - case "h1": { - return ( -
- {renderInlineToken(t.line)} -
- ); - } - case "h2": { - return ( -
- {renderInlineToken(t.line)} -
- ); - } - case "h3": { - return ( -
- {renderInlineToken(t.line)} -
- ); - } - case "p": - return ( -
- {t.lines.map((e, i) => ( - - {renderInlineToken(e.line)} - - ))} -
- ); - case "text": - return ( - <> - {renderInlineToken(t.line)} -   - - ); - case "list1": - return
  • {renderInlineToken(t.line)}
  • ; - case "list2": - return
  • {renderInlineToken(t.line)}
  • ; - default: - return ( -
    - Missing implementation for tcMD element `{(t as { type: string }) - .type}` -
    - ); - } -}; +// const renderToken = (t: Token, usedIds: string[]) => { +// switch (t.type) { +// case "h1": { +// return ( +//
    +// {renderInlineToken(t.line)} +//
    +// ); +// } +// case "h2": { +// return ( +//
    +// {renderInlineToken(t.line)} +//
    +// ); +// } +// case "h3": { +// return ( +//
    +// {renderInlineToken(t.line)} +//
    +// ); +// } +// case "p": +// return ( +//
    +// {t.lines.map((e, i) => ( +// +// {renderInlineToken(e.line)} +// +// ))} +//
    +// ); +// case "text": +// return ( +// <> +// {renderInlineToken(t.line)} +//   +// +// ); +// case "list1": +// return
  • {renderInlineToken(t.line)}
  • ; +// case "list2": +// return
  • {renderInlineToken(t.line)}
  • ; +// default: +// return ( +//
    +// Missing implementation for tcMD element `{(t as { type: string }) +// .type}` +//
    +// ); +// } +// }; -const renderInlineToken = (l: Line) => { - if (typeof l === "string") return l; +// const renderInlineToken = (l: Line) => { +// if (typeof l === "string") return l; - return l.map((token) => ( - - {(() => { - switch (token.type) { - case "text": - return {token.content}; - case "bold": - return {token.content}; - case "italic": - return {token.content}; - case "anchor": - return ( - - {token.content} - - ); - case "image": { - token.data.src = token.data.src as string; - if (token.data.src.startsWith(" -
    - ); - } - // eslint-disable-next-line @next/next/no-img-element - return {token.content}; - } - case "popover": - return ( - - - {token.content} - - - ); - case "inline-code": - return ( - - {token.content} - - ); - default: - return ( - - Inline element not implemented: {token.type} - - ); - } - })()} - - )); -}; +// return l.map((token) => ( +// +// {(() => { +// switch (token.type) { +// case "text": +// return {token.content}; +// case "bold": +// return {token.content}; +// case "italic": +// return {token.content}; +// case "anchor": +// return ( +// +// {token.content} +// +// ); +// case "image": { +// token.data.src = token.data.src as string; +// if (token.data.src.startsWith(" +// +// ); +// } +// // eslint-disable-next-line @next/next/no-img-element +// return {token.content}; +// } +// case "popover": +// return ( +// +// +// {token.content} +// +// +// ); +// case "inline-code": +// return ( +// +// {token.content} +// +// ); +// default: +// return ( +// +// Inline element not implemented: {token.type} +// +// ); +// } +// })()} +// +// )); +// }; diff --git a/lib/tcmd/TokenIdentifiers.ts b/lib/tcmd/TokenIdentifiers.ts index 51a2740..3a1072a 100644 --- a/lib/tcmd/TokenIdentifiers.ts +++ b/lib/tcmd/TokenIdentifiers.ts @@ -140,6 +140,36 @@ TokenIdentifiers.set("inline-code", { }; }, }); +TokenIdentifiers.set("bold", { + rx: /\*{2}(.*?)\*{2}/g, + parse(s) { + return { + // content: inline, + content: s.match(new RegExp(this.rx, "i"))?.at(1) || + "Unable to parse bold", + raw: s, + metadata: {}, + type: "bold", + uuid: crypto.randomUUID(), + rendersContentOnly, + }; + }, +}); +TokenIdentifiers.set("italic", { + rx: /(?>/g, parse(s) { @@ -172,6 +202,7 @@ TokenIdentifiers.set("accordion", { }, }); TokenIdentifiers.set("p", { + // rx: /(?<=\n)\n?([\s\S]*?)\n(?=\n)/g, rx: /(?<=\n\n)([\s\S]*?)(?=\n\n)/g, parse(s) { // const [_, content] = s.match(new RegExp(this.rx, ""))!; diff --git a/lib/tcmd/index.ts b/lib/tcmd/index.ts index 7da536f..8ecf9f8 100644 --- a/lib/tcmd/index.ts +++ b/lib/tcmd/index.ts @@ -3,7 +3,7 @@ import { zipArrays } from "../zip"; import { TokenIdentifiers } from "./TokenIdentifiers"; -export const createElements = (body: string): [Token[], number] => { +export const createElements = (body: string): [TokenMarker[], number] => { const tabOptions = [ /^\s{2}(?!\s|\t)/m, /^\s{4}(?!\s|\t)/m, @@ -18,13 +18,13 @@ export const createElements = (body: string): [Token[], number] => { } } const tokens = tokenize(body); - return [buildAbstractSyntaxTree(tokens, body), tabSpacing]; + return [buildAbstractSyntaxTree(tokens), tabSpacing]; }; const tokenize = (body: string) => { - const tokenizedBody: tokenMarker[] = []; + const tokenizedBody: TokenMarker[] = []; - const addToken = (thing: tokenMarker) => { + const addToken = (thing: TokenMarker) => { tokenizedBody.push(thing); }; @@ -35,145 +35,93 @@ const tokenize = (body: string) => { const start = match.index; const end = rx.lastIndex; - if (type !== "p" || !tokenizedBody.find((i) => i.start === start)) { - addToken({ - start, - end, - type, - }); - } + addToken({ + start, + end, + type, + token: token.parse(match[0]), + }); } } return tokenizedBody; }; -export const buildAbstractSyntaxTree = ( - markers: tokenMarker[], - body: string, -): Token[] => { - ensureNoOrphans(markers); +function buildAbstractSyntaxTree(markers: TokenMarker[]) { + markers.sort((a, b) => a.start - b.start); - markers.sort((a, b) => { - if (a.start === b.start) { - console.log(a, b); - if (a.type === "p") return -1; - if (b.type === "p") return 1; - } - // if (a.type === "p" && a.start === b.start) return -1; - // if (b.type === "p" && a.start === b.start) return 1; - return a.start - b.start; - }); + markers = filterOverlappingPBlocks(markers); + + establishClosestParent(markers); for (const marker of markers) { - marker.token = TokenIdentifiers.get(marker.type)?.parse( - body.substring(marker.start, marker.end), - ); - // if (marker.type === "p" && marker.parent && marker.parent?.type !== "p") { - // marker.parent = undefined; - // continue; - // } - if (!marker.token) { - throw new Error("Failed to parse token. Token type not found?"); + if (marker.parent) { + marker.parent.token.children = marker.parent.token.children || []; + marker.parent.token.children.push(marker.token); } - if (!marker.parent) continue; - - if (!marker.parent.token) { - // debugger; - throw new Error("Failed to parse token. Child tokenized before parent"); - } - - marker.parent.token.children = marker.parent.token.children || []; - marker.parent.token.children.push(marker.token); - // marker.token.parent = marker.parent.token; } - const tokens = markers.filter((m) => - markers.filter((a) => a !== m && (a.end === m.end || a.start === m.start)) - .length || m.type !== "p" - ).map((t) => t.token!); - - for (const token of tokens) { - contentToChildren(token); + // By starting at the end, we can always assure that we are not filtering out children that haven't been processed yet + for (const marker of [...markers].reverse()) { + contentToChildren(marker.token); } - return tokens.filter((t) => !t.parent); -}; + return markers.filter((m) => !m.parent); + // return markers; +} -const ensureNoOrphansOld = (tokens: tokenMarker[]) => { - for (const token of tokens) { - const parentPs = tokens.filter((t) => ( - t.type === "p" && ( - // any p that fully encapsulates the token - (t.start <= token.start && t.end >= token.end) || - // any p that contains the start of the token - (t.start <= token.start && t.end >= token.start) || - // any p that contains the end of the token - (t.start <= token.end && t.end >= token.end) - ) - )).sort((a, b) => (a.start - b.start)); +function establishClosestParent(blocks: TokenMarker[]): void { + blocks.sort((a, b) => a.start - b.start); // Sort blocks by start position - if (parentPs.length > 1) { - parentPs[0].end = parentPs.at(-1)!.end; - const remainingParents = parentPs.slice(1); - for (const token of tokens) { - if (token.parent && remainingParents.includes(token.parent)) { - token.parent = parentPs[0]; + for (let i = 0; i < blocks.length; i++) { + const block = blocks[i]; + if (block.parent) continue; // Skip blocks that already have a parent + + let closestParent: TokenMarker | undefined = undefined; + let minDistance = Number.MAX_SAFE_INTEGER; + + // Find the closest parent block for each block + for (let j = 0; j < i; j++) { + const otherBlock = blocks[j]; + if (otherBlock.end >= block.start && otherBlock.start <= block.start) { + const distance = block.start - otherBlock.start; + if (distance < minDistance) { + minDistance = distance; + closestParent = otherBlock; } } - if (parentPs[0] && parentPs[0].end < token.end) { - parentPs[0].end = token.end; - } - tokens = tokens.filter((t) => !remainingParents.includes(t)); } - const potentialParents = tokens.filter((t) => - (t.start < token.start && t.end > token.end) || - (t.type === "p" && t.start <= token.start && - t.end >= token.end && t !== token) - ).sort((a, b) => { - if (token.start - a.start < token.start - b.start) return -1; - return 1; - }); - - token.parent = potentialParents.find((p) => p.type !== "p") ?? - potentialParents[0]; - - if (token.type === "grid") { - debugger; + if (closestParent) { + block.parent = closestParent; // Assign the closest parent block } } -}; +} -const ensureNoOrphans = (tokens: tokenMarker[]) => { - ensureNoOrphansOld(tokens); -}; +function filterOverlappingPBlocks(blocks: TokenMarker[]): TokenMarker[] { + return blocks.filter((block) => { + if (block.type !== "p") { + return true; // Keep blocks that are not 'p' type + } + + // Filter out 'p' blocks that overlap with any other block + for (const otherBlock of blocks) { + if ( + otherBlock !== block && ( + otherBlock.start === block.start || + otherBlock.end === block.end + ) + ) { + return false; // Overlapping 'p' block found, filter it out + } + } + + return true; // Keep 'p' block if it doesn't overlap with any other block + }); +} const contentToChildren = (token: Token) => { - const children: Token[] = []; - let part, content = token.content; + let content = token.content; - // for (const child of token.children || []) { - // if (!content) continue; - // [part, content] = content.split(child.raw); - // part && children.push({ - // content: part.trim(), - // metadata: {}, - // raw: part, - // type: "text", - // uuid: crypto.randomUUID(), - // }); - // children.push(child); - // } - - // if (content) { - // children.push({ - // content: content.trim(), - // metadata: {}, - // raw: content, - // type: "text", - // uuid: crypto.randomUUID(), - // }); - // } const splitMarker = "{{^^}}"; for (const child of token.children || []) { content = content.replace(child.raw, splitMarker); @@ -181,7 +129,7 @@ const contentToChildren = (token: Token) => { token.children = zipArrays( content.split(splitMarker).map((c): Token => ({ - content: c.trim(), + content: c.replaceAll("\n", ""), metadata: {}, raw: c, type: "text", @@ -191,96 +139,3 @@ const contentToChildren = (token: Token) => { token.children || [], ).filter((c) => c.children?.length || (c.rendersContentOnly && c.content)); }; - -// const tokenize = (body: string) => { -// body = body.replace(/\n?\n?/gs, ""); - -// const paragraphs = body.split("\n\n"); - -// const blockTokens: BlockToken[] = []; -// const paragraphTokens: ParagraphToken[] = []; - -// for (const paragraph of paragraphs) { -// const block = tokenizeBlock(paragraph); -// let openBT = blockTokens.findLast((bt) => !bt.closed); -// if (block) { -// if (typeof block === "string") { -// if (openBT) { -// openBT.closed = true; -// } -// continue; -// } - -// if (openBT) { -// openBT.children.push(block); -// block.parent = openBT.type; -// } -// blockTokens.push(block); -// continue; -// } - -// if (!openBT) { -// openBT = { -// children: [], -// closed: false, -// metadata: {}, -// type: "block", -// uuid: crypto.randomUUID(), -// }; -// blockTokens.push(openBT); -// } - -// const multiline = tokenizeParagraph(paragraph); -// let openP = paragraphTokens.findLast((p) => !p.closed); -// if (multiline) { -// if (Array.isArray(multiline)) { -// if (openP) { -// openP.closed = true; -// openP.content = openP.content.concat(multiline); -// } -// continue; -// } - -// openBT.children.push(multiline); -// paragraphTokens.push(multiline); -// continue; -// } else if (openP && !openP?.allowsInline) { -// openP.content.push({ -// line: paragraph, -// raw: paragraph, -// type: "text", -// uuid: crypto.randomUUID(), -// }); -// } - -// // I don't think the closed check is necessary, but just in case -// // if (openP && !openP.closed && !openP.allowsInline) continue; -// if (!openP) { -// openP = { -// allowsInline: true, -// closed: true, -// content: [], -// metadata: {}, -// type: "p", -// uuid: crypto.randomUUID(), -// }; -// openBT.children.push(openP); -// paragraphTokens.push(openP); -// } - -// const lines = paragraph.split("\n"); -// let previous; -// for (const line of lines) { -// const singleLine = tokenizeLine(line, previous); - -// if (singleLine) { -// if (singleLine !== previous) { -// openP.content.push(singleLine); -// } -// previous = singleLine; -// } -// } -// } - -// return blockTokens.filter((b) => !b.parent); -// }; diff --git a/md/help articles/How to use ttcMD.md b/md/help articles/How to use ttcMD.md index d833dba..f9680ed 100644 --- a/md/help articles/How to use ttcMD.md +++ b/md/help articles/How to use ttcMD.md @@ -21,12 +21,14 @@ This section will cover all of the enhancements that are added for basic markdow You can use the typical link syntax: `[link name](/link/location)`, but there are a few presets that allow you to style them to look a bit nicer. **Primary Button:** + Prefix the link name with ````button` to create a button. `[```button link name](#links)` produces: [```button link name](#links) **Call to Action:** + Prefix the link name with ````cta` to create a modestly styled button/call to action. `[```cta link name](#links)` produces: @@ -59,7 +61,6 @@ Accordions are when you can click an item to expand it to show additional inform Syntax: [][][] - ``` [accordion title] @@ -77,11 +78,7 @@ As you can see, I can do normal markdown in here. I can include a [link](#accordions), or *italic* and **bold** text. [[ - I can even include a card, like this one - ]] - [/accordion] - /[] \ No newline at end of file diff --git a/types.d.ts b/types.d.ts index 918f626..87707b5 100644 --- a/types.d.ts +++ b/types.d.ts @@ -42,7 +42,6 @@ type SingleLineToken = { type Token = { type: string; metadata: Record; - parent?: Token; children?: Token[]; uuid: string; raw: string; @@ -51,12 +50,12 @@ type Token = { rendersContentOnly?: boolean; }; -type tokenMarker = { +type TokenMarker = { start: number; end: number; type: string; - parent?: tokenMarker; - token?: Token; + parent?: TokenMarker; + token: Token; }; type MultilineCfg = {