diff --git a/build.js b/build.js index e52ae31..d1481ab 100644 --- a/build.js +++ b/build.js @@ -1,6 +1,179 @@ const fs = require('fs').promises; const path = require('path'); +/** + * Parse YAML front matter from markdown content + * @param {string} content - The markdown file content + * @returns {{metadata: Object, content: string}} + */ +function parseMarkdownWithFrontmatter(content) { + const frontmatterRegex = /^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/; + const match = content.match(frontmatterRegex); + + if (!match) { + return { metadata: {}, content: content }; + } + + const yamlContent = match[1]; + const markdownContent = match[2]; + + // Simple YAML parser for basic key: value pairs + const metadata = {}; + const lines = yamlContent.split(/\r?\n/); + for (const line of lines) { + const colonIndex = line.indexOf(':'); + if (colonIndex > 0) { + const key = line.substring(0, colonIndex).trim(); + const value = line.substring(colonIndex + 1).trim(); + // Remove quotes if present + metadata[key] = value.replace(/^["']|["']$/g, ''); + } + } + + return { metadata, content: markdownContent }; +} + +/** + * Process blog markdown files and generate blogs.json, also update graph.json + */ +async function processBlogPosts() { + const blogDir = path.join(__dirname, 'website', 'data', 'blog'); + const outputPath = path.join(__dirname, 'website', 'data', 'blogs.json'); + const graphPath = path.join(__dirname, 'website', 'data', 'graph.json'); + + try { + const files = await fs.readdir(blogDir); + const mdFiles = files.filter(f => f.endsWith('.md')); + + const posts = []; + + for (const file of mdFiles) { + const filePath = path.join(blogDir, file); + const content = await fs.readFile(filePath, 'utf-8'); + const { metadata, content: markdown } = parseMarkdownWithFrontmatter(content); + + const slug = path.basename(file, '.md'); + + // Extract and process tags separately + const tags = metadata.tags ? metadata.tags.split(',').map(t => t.trim()) : []; + + // Remove processed fields from metadata + const { tags: _tags, title, date, author, ...extraMetadata } = metadata; + + posts.push({ + slug, + title: title || slug, + date: date || null, + author: author || null, + tags, + content: markdown, + ...extraMetadata + }); + } + + // Sort by date descending (newest first) + posts.sort((a, b) => { + if (!a.date) return 1; + if (!b.date) return -1; + return new Date(b.date) - new Date(a.date); + }); + + await fs.writeFile(outputPath, JSON.stringify(posts, null, 2)); + console.log(`✓ Processed ${posts.length} blog post(s)`); + + // Update graph.json with blog nodes + if (posts.length > 0) { + await updateGraphWithBlogNodes(posts, graphPath); + } + } catch (err) { + if (err.code === 'ENOENT') { + console.log('⚠ No blog directory found, skipping blog processing'); + } else { + throw err; + } + } +} + +/** + * Update graph.json with blog post nodes + * @param {Array} posts - Array of blog post objects + * @param {string} graphPath - Path to graph.json + */ +async function updateGraphWithBlogNodes(posts, graphPath) { + const graphData = JSON.parse(await fs.readFile(graphPath, 'utf-8')); + + // Remove existing blog nodes and their connections + graphData.nodes = graphData.nodes.filter(node => node.type !== 'blog_post'); + graphData.connections = graphData.connections.filter(conn => { + const fromNode = graphData.nodes.find(n => n.id === conn.from.nodeId); + const toNode = graphData.nodes.find(n => n.id === conn.to.nodeId); + return fromNode && toNode; + }); + + const startX = 1500; + const startY = 900; + const horizontalSpacing = 700; + const nodeWidth = 600; + + const blogNodes = []; + const blogConnections = []; + + posts.forEach((post, index) => { + const nodeId = `blog_${post.slug}`; + const x = startX + (index * horizontalSpacing); + + const node = { + id: nodeId, + type: 'blog_post', + title: post.title, + blogSlug: post.slug, + position: { + x: x, + y: startY + }, + width: nodeWidth, + inputs: index > 0 ? [{ + id: 'exec_in', + name: 'Previous', + direction: 'input', + kind: 'exec' + }] : [], + outputs: index < posts.length - 1 ? [{ + id: 'exec_out', + name: 'Next', + direction: 'output', + kind: 'exec' + }] : [] + }; + + blogNodes.push(node); + + // Create connection to next post + if (index < posts.length - 1) { + const nextNodeId = `blog_${posts[index + 1].slug}`; + blogConnections.push({ + id: `blog_connection_${index}`, + from: { + nodeId: nodeId, + pinId: 'exec_out' + }, + to: { + nodeId: nextNodeId, + pinId: 'exec_in' + }, + kind: 'exec' + }); + } + }); + + // Add blog nodes and connections to graph + graphData.nodes.push(...blogNodes); + graphData.connections.push(...blogConnections); + + await fs.writeFile(graphPath, JSON.stringify(graphData, null, 2)); + console.log(`✓ Added ${blogNodes.length} blog node(s) to graph`); +} + // Copy files recursively from website to build async function copyWebsiteFiles() { const websiteDir = path.join(__dirname, 'website'); @@ -43,4 +216,9 @@ async function copyWebsiteFiles() { } // Run the build -copyWebsiteFiles(); +async function build() { + await processBlogPosts(); + await copyWebsiteFiles(); +} + +build(); diff --git a/website/data/blog/markdown-test.md b/website/data/blog/markdown-test.md new file mode 100644 index 0000000..e60bb76 --- /dev/null +++ b/website/data/blog/markdown-test.md @@ -0,0 +1,125 @@ +--- +title: Markdown Formatting Test +date: 2026-04-20 +author: Test Author +tags: test, formatting, markdown +--- + +# Heading 1 + +## Heading 2 + +### Heading 3 + +#### Heading 4 + +##### Heading 5 + +###### Heading 6 + +## Text Formatting + +This is **bold text** and this is *italic text* and this is ***bold italic text***. + +Here's some `inline code` in a sentence. + +## Links + +Here's a [regular link](https://example.com) and here's a [YouTube link](https://youtube.com/watch?v=test). + +## Lists + +Unordered list: +- First item +- Second item +- Third item + +Ordered list: +1. First step +2. Second step +3. Third step + +## Code Blocks + +Standard JavaScript: +```javascript +const hello = "world"; +console.log(hello); + +function test() { + return true; +} +``` + +Python with syntax highlighting: +```python +def hello_world(): + print("Hello, World!") + return True +``` + +Code with max height (200px): +```javascript {maxHeight: 200} +// This is a long code block that will scroll +const data = [1, 2, 3, 4, 5]; + +function processData(arr) { + return arr.map(x => x * 2); +} + +console.log(processData(data)); + +// Adding more lines to demonstrate scrolling +for (let i = 0; i < 10; i++) { + console.log(`Iteration ${i}`); +} + +// Even more content +const obj = { + name: "Test", + value: 42, + nested: { + deep: true + } +}; +``` + +## Blockquotes + +> This is a blockquote. +> It can span multiple lines. +> And continues here. + +## Horizontal Rule + +--- + +## Images + + + +## Mixed Content + +You can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with **bold** text](https://example.com) too. + +### Nested Lists + +- Top level item +- Another top level + - Nested item (if supported) + - Another nested + +1. Numbered item +2. Another numbered + - Mixed with bullets + - More bullets + +## Special Characters + +Testing special chars: < > & " ' + +## Long Paragraph + +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. + +Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/website/data/blog/node-graphs.md b/website/data/blog/node-graphs.md new file mode 100644 index 0000000..c198800 --- /dev/null +++ b/website/data/blog/node-graphs.md @@ -0,0 +1,52 @@ +--- +title: Building Interactive Node Graphs +date: 2026-04-15 +author: Max Litruv Boonzaayer +tags: javascript, graph-systems, tutorial +--- + +# Building Interactive Node Graphs + +In this post, I'll share some insights on building interactive node-based graph systems for the web. + +## Why Node Graphs? + +Node graphs are powerful visual programming tools that make complex logic easier to understand and manipulate: + +- **Visual Clarity**: See the flow of data and execution at a glance +- **Modularity**: Each node is a self-contained unit +- **Flexibility**: Easy to add new node types and behaviors + +## The Architecture + +The system is built on several key classes: + +### NodeBase +The abstract base class that all nodes inherit from. It defines the blueprint interface: + +```javascript +class NodeBase { + static NodeType = ""; + static BlueprintPure_GetDefaultPins() { } + BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) { } + async BlueprintCallable_Execute(ctx) { } +} +``` + +### Node Registry +Automatically registers node types and creates instances based on type strings. + +### Execution Context +Handles the graph traversal and execution flow when nodes are triggered. + +## Creating Custom Nodes + +To create a new node type: + +1. Extend `NodeBase` +2. Set a unique `NodeType` +3. Define pins with `BlueprintPure_GetDefaultPins()` +4. Implement rendering in `BlueprintNativeEvent_OnRender()` +5. Implement logic in `BlueprintCallable_Execute()` + +That's the blueprint system in a nutshell! diff --git a/website/data/blog/welcome.md b/website/data/blog/welcome.md new file mode 100644 index 0000000..8251e31 --- /dev/null +++ b/website/data/blog/welcome.md @@ -0,0 +1,46 @@ +--- +title: Welcome to My Blog +date: 2026-04-19 +author: Max Litruv Boonzaayer +tags: welcome, meta, introduction +--- + +# Welcome to My Blog + +This is my first blog post! I'm excited to share my thoughts and experiences with you. + +## What This Blog Is About + +This blog is built using a **custom node-based graph system** where each blog post is represented as a node in an interactive visual graph. Pretty cool, right? + +### Features + +- 📝 Markdown support with YAML front matter +- 📅 Date-based organization +- 🏷️ Tag system for categorization +- 🎨 Interactive node-based visualization +- 📁 Media support in `data/blog/media/` + +## Technical Details + +The build system automatically: +1. Scans the `data/blog/` directory for `.md` files +2. Parses YAML front matter for metadata +3. Generates a `blogs.json` file +4. Makes posts available to the BlogPostNode + +You can reference images like this: + + +## Code Examples + +Here's some example code: + +```javascript +const greeting = "Hello, World!"; +console.log(greeting); +``` + +## Conclusion + +Stay tuned for more posts! This is just the beginning of an exciting journey. diff --git a/website/data/blogs.json b/website/data/blogs.json new file mode 100644 index 0000000..1d36227 --- /dev/null +++ b/website/data/blogs.json @@ -0,0 +1,38 @@ +[ + { + "slug": "markdown-test", + "title": "Markdown Formatting Test", + "date": "2026-04-20", + "author": "Test Author", + "tags": [ + "test", + "formatting", + "markdown" + ], + "content": "\r\n# Heading 1\r\n\r\n## Heading 2\r\n\r\n### Heading 3\r\n\r\n#### Heading 4\r\n\r\n##### Heading 5\r\n\r\n###### Heading 6\r\n\r\n## Text Formatting\r\n\r\nThis is **bold text** and this is *italic text* and this is ***bold italic text***.\r\n\r\nHere's some `inline code` in a sentence.\r\n\r\n## Links\r\n\r\nHere's a [regular link](https://example.com) and here's a [YouTube link](https://youtube.com/watch?v=test).\r\n\r\n## Lists\r\n\r\nUnordered list:\r\n- First item\r\n- Second item\r\n- Third item\r\n\r\nOrdered list:\r\n1. First step\r\n2. Second step\r\n3. Third step\r\n\r\n## Code Blocks\r\n\r\nStandard JavaScript:\r\n```javascript\r\nconst hello = \"world\";\r\nconsole.log(hello);\r\n\r\nfunction test() {\r\n return true;\r\n}\r\n```\r\n\r\nPython with syntax highlighting:\r\n```python\r\ndef hello_world():\r\n print(\"Hello, World!\")\r\n return True\r\n```\r\n\r\nCode with max height (200px):\r\n```javascript {maxHeight: 200}\r\n// This is a long code block that will scroll\r\nconst data = [1, 2, 3, 4, 5];\r\n\r\nfunction processData(arr) {\r\n return arr.map(x => x * 2);\r\n}\r\n\r\nconsole.log(processData(data));\r\n\r\n// Adding more lines to demonstrate scrolling\r\nfor (let i = 0; i < 10; i++) {\r\n console.log(`Iteration ${i}`);\r\n}\r\n\r\n// Even more content\r\nconst obj = {\r\n name: \"Test\",\r\n value: 42,\r\n nested: {\r\n deep: true\r\n }\r\n};\r\n```\r\n\r\n## Blockquotes\r\n\r\n> This is a blockquote.\r\n> It can span multiple lines.\r\n> And continues here.\r\n\r\n## Horizontal Rule\r\n\r\n---\r\n\r\n## Images\r\n\r\n\r\n\r\n## Mixed Content\r\n\r\nYou can mix **bold**, *italic*, and `code` in the same paragraph. Here's a [link with **bold** text](https://example.com) too.\r\n\r\n### Nested Lists\r\n\r\n- Top level item\r\n- Another top level\r\n - Nested item (if supported)\r\n - Another nested\r\n\r\n1. Numbered item\r\n2. Another numbered\r\n - Mixed with bullets\r\n - More bullets\r\n\r\n## Special Characters\r\n\r\nTesting special chars: < > & \" ' \r\n\r\n## Long Paragraph\r\n\r\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\r\n\r\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\r\n" + }, + { + "slug": "welcome", + "title": "Welcome to My Blog", + "date": "2026-04-19", + "author": "Max Litruv Boonzaayer", + "tags": [ + "welcome", + "meta", + "introduction" + ], + "content": "\r\n# Welcome to My Blog\r\n\r\nThis is my first blog post! I'm excited to share my thoughts and experiences with you.\r\n\r\n## What This Blog Is About\r\n\r\nThis blog is built using a **custom node-based graph system** where each blog post is represented as a node in an interactive visual graph. Pretty cool, right?\r\n\r\n### Features\r\n\r\n- 📝 Markdown support with YAML front matter\r\n- 📅 Date-based organization\r\n- 🏷️ Tag system for categorization\r\n- 🎨 Interactive node-based visualization\r\n- 📁 Media support in `data/blog/media/`\r\n\r\n## Technical Details\r\n\r\nThe build system automatically:\r\n1. Scans the `data/blog/` directory for `.md` files\r\n2. Parses YAML front matter for metadata\r\n3. Generates a `blogs.json` file\r\n4. Makes posts available to the BlogPostNode\r\n\r\nYou can reference images like this:\r\n\r\n\r\n## Code Examples\r\n\r\nHere's some example code:\r\n\r\n```javascript\r\nconst greeting = \"Hello, World!\";\r\nconsole.log(greeting);\r\n```\r\n\r\n## Conclusion\r\n\r\nStay tuned for more posts! This is just the beginning of an exciting journey.\r\n" + }, + { + "slug": "node-graphs", + "title": "Building Interactive Node Graphs", + "date": "2026-04-15", + "author": "Max Litruv Boonzaayer", + "tags": [ + "javascript", + "graph-systems", + "tutorial" + ], + "content": "\r\n# Building Interactive Node Graphs\r\n\r\nIn this post, I'll share some insights on building interactive node-based graph systems for the web.\r\n\r\n## Why Node Graphs?\r\n\r\nNode graphs are powerful visual programming tools that make complex logic easier to understand and manipulate:\r\n\r\n- **Visual Clarity**: See the flow of data and execution at a glance\r\n- **Modularity**: Each node is a self-contained unit\r\n- **Flexibility**: Easy to add new node types and behaviors\r\n\r\n## The Architecture\r\n\r\nThe system is built on several key classes:\r\n\r\n### NodeBase\r\nThe abstract base class that all nodes inherit from. It defines the blueprint interface:\r\n\r\n```javascript\r\nclass NodeBase {\r\n static NodeType = \"\";\r\n static BlueprintPure_GetDefaultPins() { }\r\n BlueprintNativeEvent_OnRender(article, graphNode, renderCtx) { }\r\n async BlueprintCallable_Execute(ctx) { }\r\n}\r\n```\r\n\r\n### Node Registry\r\nAutomatically registers node types and creates instances based on type strings.\r\n\r\n### Execution Context\r\nHandles the graph traversal and execution flow when nodes are triggered.\r\n\r\n## Creating Custom Nodes\r\n\r\nTo create a new node type:\r\n\r\n1. Extend `NodeBase`\r\n2. Set a unique `NodeType`\r\n3. Define pins with `BlueprintPure_GetDefaultPins()`\r\n4. Implement rendering in `BlueprintNativeEvent_OnRender()`\r\n5. Implement logic in `BlueprintCallable_Execute()`\r\n\r\nThat's the blueprint system in a nutshell!\r\n" + } +] \ No newline at end of file diff --git a/website/data/graph.json b/website/data/graph.json index 57256d9..999d476 100644 --- a/website/data/graph.json +++ b/website/data/graph.json @@ -94,8 +94,8 @@ "type": "sequence", "title": "Sequence", "position": { - "x": 1180, - "y": 120 + "x": 1080, + "y": 20 }, "focusOptions": { "minWorldBox": { @@ -137,8 +137,8 @@ "type": "info", "title": "Who Am I", "position": { - "x": 1780, - "y": -640 + "x": 1600, + "y": -860 }, "width": 340, "markdownSrc": "data/whoami.md", @@ -186,8 +186,8 @@ "type": "info", "title": "Find Me Online", "position": { - "x": 2460, - "y": 140 + "x": 2420, + "y": -280 }, "markdownSrc": "data/socials.md", "focusOptions": { @@ -211,8 +211,8 @@ "type": "pure", "title": "String Node", "position": { - "x": 2260, - "y": -280 + "x": 2180, + "y": -500 }, "inputs": [], "outputs": [ @@ -230,8 +230,8 @@ "type": "button", "title": "Run", "position": { - "x": 2180, - "y": -360 + "x": 2100, + "y": -600 }, "inputs": [], "outputs": [ @@ -248,8 +248,8 @@ "type": "timer", "title": "Timer", "position": { - "x": 2180, - "y": -460 + "x": 2100, + "y": -700 }, "inputs": [ { @@ -276,8 +276,8 @@ "type": "print", "title": "Print String", "position": { - "x": 2540, - "y": -460 + "x": 2380, + "y": -680 }, "inputs": [ { @@ -308,8 +308,8 @@ "type": "pure", "title": "Homeserver", "position": { - "x": 1520, - "y": 1020 + "x": 1480, + "y": 340 }, "inputs": [], "outputs": [ @@ -327,8 +327,8 @@ "type": "pure", "title": "Room", "position": { - "x": 1520, - "y": 1100 + "x": 1480, + "y": 420 }, "inputs": [], "outputs": [ @@ -346,8 +346,8 @@ "type": "chat", "title": "Matrix Chat", "position": { - "x": 1800, - "y": 920 + "x": 1740, + "y": 220 }, "width": 400, "focusOptions": { @@ -420,8 +420,8 @@ "type": "random_name", "title": "Random Name", "position": { - "x": 1520, - "y": 1180 + "x": 1480, + "y": 500 }, "inputs": [], "outputs": [ @@ -438,8 +438,8 @@ "type": "button", "title": "Connect Chat", "position": { - "x": 1960, - "y": 820 + "x": 1920, + "y": 120 }, "inputs": [], "outputs": [ @@ -456,8 +456,8 @@ "type": "chat_connect", "title": "Connect Chat", "position": { - "x": 2220, - "y": 880 + "x": 2260, + "y": 220 }, "inputs": [ { @@ -487,8 +487,8 @@ "type": "get_matrix_chat", "title": "Get MatrixChat", "position": { - "x": 1520, - "y": 940 + "x": 1500, + "y": 240 }, "inputs": [], "outputs": [ @@ -506,8 +506,8 @@ "type": "bind_event", "title": "Bind: Chat On Message", "position": { - "x": 2460, - "y": 880 + "x": 2540, + "y": 220 }, "inputs": [ { @@ -557,8 +557,8 @@ "type": "append", "title": "Append", "position": { - "x": 2700, - "y": 960 + "x": 2780, + "y": 280 }, "inputs": [ { @@ -597,8 +597,8 @@ "type": "print", "title": "Print Chat Message", "position": { - "x": 2940, - "y": 900 + "x": 3040, + "y": 220 }, "inputs": [ { @@ -623,6 +623,73 @@ "kind": "exec" } ] + }, + { + "id": "blog_markdown-test", + "type": "blog_post", + "title": "Markdown Formatting Test", + "blogSlug": "markdown-test", + "position": { + "x": 1500, + "y": 900 + }, + "width": 600, + "inputs": [], + "outputs": [ + { + "id": "exec_out", + "name": "Next", + "direction": "output", + "kind": "exec" + } + ] + }, + { + "id": "blog_welcome", + "type": "blog_post", + "title": "Welcome to My Blog", + "blogSlug": "welcome", + "position": { + "x": 2200, + "y": 900 + }, + "width": 600, + "inputs": [ + { + "id": "exec_in", + "name": "Previous", + "direction": "input", + "kind": "exec" + } + ], + "outputs": [ + { + "id": "exec_out", + "name": "Next", + "direction": "output", + "kind": "exec" + } + ] + }, + { + "id": "blog_node-graphs", + "type": "blog_post", + "title": "Building Interactive Node Graphs", + "blogSlug": "node-graphs", + "position": { + "x": 2900, + "y": 900 + }, + "width": 600, + "inputs": [ + { + "id": "exec_in", + "name": "Previous", + "direction": "input", + "kind": "exec" + } + ], + "outputs": [] } ], "connections": [ @@ -853,6 +920,30 @@ "pinId": "target" }, "kind": "object" + }, + { + "id": "blog_connection_0", + "from": { + "nodeId": "blog_markdown-test", + "pinId": "exec_out" + }, + "to": { + "nodeId": "blog_welcome", + "pinId": "exec_in" + }, + "kind": "exec" + }, + { + "id": "blog_connection_1", + "from": { + "nodeId": "blog_welcome", + "pinId": "exec_out" + }, + "to": { + "nodeId": "blog_node-graphs", + "pinId": "exec_in" + }, + "kind": "exec" } ] } \ No newline at end of file diff --git a/website/index.html b/website/index.html index 2640e2e..8edaa5d 100644 --- a/website/index.html +++ b/website/index.html @@ -30,6 +30,15 @@ + + + + + + + + +
diff --git a/website/scripts/GraphNode.js b/website/scripts/GraphNode.js index 768a185..66085f6 100644 --- a/website/scripts/GraphNode.js +++ b/website/scripts/GraphNode.js @@ -14,7 +14,7 @@ */ export class GraphNode { /** - * @param {{ id: string, type: string, title: string, position: {x:number,y:number}, width?: number, inputs: PinDescriptor[], outputs: PinDescriptor[], markdownSrc?: string, imageSrc?: string, lottieSrc?: string, userSpawned?: boolean, focusOptions?: { paddingFraction?: number, durationMs?: number, minWorldBox?: { width: number, height: number }, responsiveWorldBox?: { minViewportWidth: number, minWorldBox: { width: number, height: number }, anchorX?: number, anchorY?: number } | Array<{ minViewportWidth: number, minWorldBox: { width: number, height: number }, anchorX?: number, anchorY?: number }>, anchorX?: number, anchorY?: number } }} init + * @param {{ id: string, type: string, title: string, position: {x:number,y:number}, width?: number, inputs: PinDescriptor[], outputs: PinDescriptor[], markdownSrc?: string, imageSrc?: string, lottieSrc?: string, blogSlug?: string, userSpawned?: boolean, focusOptions?: { paddingFraction?: number, durationMs?: number, minWorldBox?: { width: number, height: number }, responsiveWorldBox?: { minViewportWidth: number, minWorldBox: { width: number, height: number }, anchorX?: number, anchorY?: number } | Array<{ minViewportWidth: number, minWorldBox: { width: number, height: number }, anchorX?: number, anchorY?: number }>, anchorX?: number, anchorY?: number } }} init */ constructor(init) { this.id = init.id; @@ -31,6 +31,8 @@ export class GraphNode { this.imageSrc = init.imageSrc; /** @type {string | undefined} */ this.lottieSrc = init.lottieSrc; + /** @type {string | undefined} */ + this.blogSlug = init.blogSlug; /** @type {boolean | undefined} */ this.userSpawned = init.userSpawned; /** @type {{ paddingFraction?: number, durationMs?: number, minWorldBox?: { width: number, height: number }, anchorX?: number, anchorY?: number } | undefined} */ diff --git a/website/scripts/MarkdownRenderer.js b/website/scripts/MarkdownRenderer.js index 9426d78..d1cf0ea 100644 --- a/website/scripts/MarkdownRenderer.js +++ b/website/scripts/MarkdownRenderer.js @@ -37,9 +37,22 @@ export class MarkdownRenderer { .replace(/\r\n/g, "\n") .replace(/\r/g, "\n"); + // Extract and protect code blocks first (prevents splitting on blank lines inside code) + const codeBlocks = []; + html = html.replace(/```[\s\S]*?```/g, (match) => { + const placeholder = `\n___CODEBLOCK_${codeBlocks.length}___\n`; + codeBlocks.push(match); + return placeholder; + }); + // Split into blocks separated by blank lines const blocks = html.split(/\n{2,}/); - const parts = blocks.map(block => MarkdownRenderer.#renderBlock(block.trim())); + const parts = blocks.map(block => { + block = block.trim(); + // Restore code blocks + block = block.replace(/___CODEBLOCK_(\d+)___/g, (_, idx) => codeBlocks[parseInt(idx)]); + return MarkdownRenderer.#renderBlock(block); + }); return parts.filter(Boolean).join("\n"); } @@ -53,11 +66,32 @@ export class MarkdownRenderer { if (!block) return ""; // Fenced code block - const fenceMatch = block.match(/^```(\w*)\n([\s\S]*?)```$/); + const fenceMatch = block.match(/^```(\w*)(?:\s*\{([^}]+)\})?\s*\n([\s\S]*?)\n?```$/); if (fenceMatch) { - const lang = MarkdownRenderer.#escape(fenceMatch[1] || ""); - const code = MarkdownRenderer.#escape(fenceMatch[2]); - return `${code}`;
+ const lang = fenceMatch[1] || "";
+ const options = fenceMatch[2] || "";
+ const code = fenceMatch[3];
+
+ // Parse options (e.g., {maxHeight: 300})
+ let maxHeight = null;
+ if (options) {
+ const maxHeightMatch = options.match(/maxHeight\s*:\s*(\d+)/);
+ if (maxHeightMatch) {
+ maxHeight = maxHeightMatch[1];
+ }
+ }
+
+ // Use Prism for syntax highlighting if available and language is specified
+ let highlightedCode = code;
+ if (lang && typeof Prism !== 'undefined' && Prism.languages[lang]) {
+ highlightedCode = Prism.highlight(code, Prism.languages[lang], lang);
+ } else {
+ highlightedCode = MarkdownRenderer.#escape(code);
+ }
+
+ const langClass = lang ? ` language-${lang}` : "";
+ const styleAttr = maxHeight ? ` style="max-height: ${maxHeight}px; overflow-y: auto;"` : "";
+ return `${highlightedCode}`;
}
// Horizontal rule
@@ -66,7 +100,7 @@ export class MarkdownRenderer {
}
// Headings
- const headingMatch = block.match(/^(#{1,3})\s+(.+)$/m);
+ const headingMatch = block.match(/^(#{1,6})\s+(.+)$/m);
if (headingMatch && block.split("\n").length === 1) {
const level = headingMatch[1].length;
return `Blog post "${graphNode.blogSlug}" not found.
`; + return; + } + + // Render metadata header + const metaDiv = document.createElement("div"); + metaDiv.className = "blog-meta"; + metaDiv.style.cssText = "margin-bottom: 1em; padding-bottom: 0.5em; border-bottom: 1px solid rgba(255,255,255,0.1);"; + + if (post.title) { + const titleEl = document.createElement("h2"); + titleEl.textContent = post.title; + titleEl.style.cssText = "margin: 0 0 0.5em 0; font-size: 1.5em;"; + metaDiv.appendChild(titleEl); + } + + const metaInfo = []; + if (post.date) { + const dateObj = new Date(post.date); + metaInfo.push(`📅 ${dateObj.toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}`); + } + if (post.author) { + metaInfo.push(`✍️ ${post.author}`); + } + + if (metaInfo.length > 0) { + const infoEl = document.createElement("div"); + infoEl.style.cssText = "color: rgba(255,255,255,0.6); font-size: 0.9em;"; + infoEl.textContent = metaInfo.join(' • '); + metaDiv.appendChild(infoEl); + } + + if (post.tags && post.tags.length > 0) { + const tagsDiv = document.createElement("div"); + tagsDiv.style.cssText = "margin-top: 0.5em;"; + post.tags.forEach(tag => { + const tagEl = document.createElement("span"); + tagEl.textContent = `#${tag}`; + tagEl.style.cssText = "display: inline-block; margin-right: 0.5em; padding: 0.2em 0.5em; background: rgba(100,150,255,0.2); border-radius: 3px; font-size: 0.85em;"; + tagsDiv.appendChild(tagEl); + }); + metaDiv.appendChild(tagsDiv); + } + + body.appendChild(metaDiv); + + // Render markdown content directly + const contentDiv = document.createElement("div"); + contentDiv.className = "blog-content"; + body.appendChild(contentDiv); + + // Import and use MarkdownRenderer directly + const { MarkdownRenderer } = await import('../MarkdownRenderer.js'); + contentDiv.innerHTML = MarkdownRenderer.render(post.content); + + } catch (error) { + body.innerHTML = `Error loading blog post: ${error.message}
`; + console.error('Failed to load blog post:', error); + } + }; + + article.appendChild(body); + loadBlogData(); + } +} + +NodeRegistry.UCLASS_Register(BlogPostNode); diff --git a/website/scripts/nodes/index.js b/website/scripts/nodes/index.js index be74562..24afcc3 100644 --- a/website/scripts/nodes/index.js +++ b/website/scripts/nodes/index.js @@ -16,6 +16,7 @@ export { CustomEventNode } from './CustomEventNode.js'; export { ButtonNode } from './ButtonNode.js'; export { TimerNode } from './TimerNode.js'; export { InfoNode } from './InfoNode.js'; +export { BlogPostNode } from './BlogPostNode.js'; export { PureNode } from './PureNode.js'; export { MatrixChatNode } from './MatrixChatNode.js'; export { GetMatrixChatNode } from './GetMatrixChatNode.js'; diff --git a/website/styles/main.css b/website/styles/main.css index b928d60..ead079a 100644 --- a/website/styles/main.css +++ b/website/styles/main.css @@ -1332,6 +1332,26 @@ svg.world-image { line-height: 1.6; user-select: text; cursor: text; + overflow-y: visible; + overflow-x: hidden; +} + +.node-body::-webkit-scrollbar { + width: 8px; +} + +.node-body::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.2); + border-radius: 4px; +} + +.node-body::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.2); + border-radius: 4px; +} + +.node-body::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.3); } .node-body .md-h1, @@ -1385,10 +1405,95 @@ svg.world-image { border-radius: 5px; padding: 0.5em 0.7em; overflow-x: auto; + overflow-y: auto; margin: 0.4em 0; font-family: "Cascadia Code", "Fira Code", ui-monospace, monospace; font-size: 0.76em; color: #a8daff; + scroll-behavior: smooth; +} + +.node-body .md-pre::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.node-body .md-pre::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.3); + border-radius: 3px; +} + +.node-body .md-pre::-webkit-scrollbar-thumb { + background: rgba(255, 255, 255, 0.15); + border-radius: 3px; +} + +.node-body .md-pre::-webkit-scrollbar-thumb:hover { + background: rgba(255, 255, 255, 0.25); +} + +.node-body .md-pre code { + background: none; + padding: 0; + border-radius: 0; + font-size: inherit; + font-family: inherit; +} + +/* Prism syntax highlighting overrides */ +.node-body .md-pre .token.comment, +.node-body .md-pre .token.prolog, +.node-body .md-pre .token.doctype, +.node-body .md-pre .token.cdata { + color: #6a9955; +} + +.node-body .md-pre .token.punctuation { + color: #d4d4d4; +} + +.node-body .md-pre .token.property, +.node-body .md-pre .token.tag, +.node-body .md-pre .token.boolean, +.node-body .md-pre .token.number, +.node-body .md-pre .token.constant, +.node-body .md-pre .token.symbol, +.node-body .md-pre .token.deleted { + color: #b5cea8; +} + +.node-body .md-pre .token.selector, +.node-body .md-pre .token.attr-name, +.node-body .md-pre .token.string, +.node-body .md-pre .token.char, +.node-body .md-pre .token.builtin, +.node-body .md-pre .token.inserted { + color: #ce9178; +} + +.node-body .md-pre .token.operator, +.node-body .md-pre .token.entity, +.node-body .md-pre .token.url, +.node-body .md-pre .language-css .token.string, +.node-body .md-pre .style .token.string { + color: #d4d4d4; +} + +.node-body .md-pre .token.atrule, +.node-body .md-pre .token.attr-value, +.node-body .md-pre .token.keyword { + color: #569cd6; +} + +.node-body .md-pre .token.function, +.node-body .md-pre .token.class-name { + color: #dcdcaa; +} + +.node-body .md-pre .token.regex, +.node-body .md-pre .token.important, +.node-body .md-pre .token.variable { + color: #9cdcfe; } .node-body .md-hr {