UNPKG

@zerospacegg/iolin

Version:

Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)

218 lines 8.11 kB
/** * ZeroSpace.gg Markdown Processing Engine * * Advanced markdown processing with AST transformations for game documentation. * Supports smart header shifting, entity auto-linking, and custom components. */ import { unified } from 'unified'; import remarkParse from 'remark-parse'; import remarkStringify from 'remark-stringify'; import { visit } from 'unist-util-visit'; /** * Plugin: Smart header level shifting * * Automatically detects the minimum heading level in content and shifts * all headings so the minimum becomes the target level (default: h3). * * Examples: * - h1,h2 content → h3,h4 (shift +2) * - h2,h3 content → h3,h4 (shift +1) * - h3,h4 content → h3,h4 (no shift) */ const remarkSmartHeaderShift = (targetDepth = 3) => { return (tree) => { let minDepth = Infinity; const headings = []; // First pass: find minimum heading depth and collect all headings visit(tree, 'heading', (node) => { minDepth = Math.min(minDepth, node.depth); headings.push(node); }); // If no headings found, nothing to do if (minDepth === Infinity) return; // Calculate shift amount to make minimum heading = targetDepth const shift = targetDepth - minDepth; // Second pass: apply the shift (but don't exceed h6) if (shift !== 0) { visit(tree, 'heading', (node) => { const newDepth = Math.min(6, node.depth + shift); node.depth = newDepth; }); } }; }; /** * Plugin: Entity auto-linking * * Converts [[entity:id]] patterns into proper markdown links to zerospace.gg * Example: [[entity:faction/grell/unit/stinger]] → [Stinger](https://zerospace.gg/library/faction/grell/unit/stinger/) */ const remarkEntityAutoLink = () => { return (tree) => { visit(tree, 'text', (node, index, parent) => { if (!parent || typeof index !== 'number') return; const text = node.value; const entityPattern = /\[\[entity:([^\]]+)\]\]/g; if (!entityPattern.test(text)) return; // Split text and replace entity references const parts = []; let lastIndex = 0; let match; // Reset regex entityPattern.lastIndex = 0; while ((match = entityPattern.exec(text)) !== null) { // Add text before the match if (match.index > lastIndex) { parts.push({ type: 'text', value: text.slice(lastIndex, match.index) }); } // Add the entity link const entityId = match[1]; const entityName = entityId.split('/').pop() || entityId; const displayName = entityName.charAt(0).toUpperCase() + entityName.slice(1).replace(/-/g, ' '); parts.push({ type: 'link', url: `https://zerospace.gg/library/${entityId}/`, children: [{ type: 'text', value: displayName }] }); lastIndex = match.index + match[0].length; } // Add remaining text if (lastIndex < text.length) { parts.push({ type: 'text', value: text.slice(lastIndex) }); } // Replace the text node with the new nodes if (parts.length > 1) { parent.children.splice(index, 1, ...parts); } }); }; }; /** * Plugin: Custom component detection * * Detects and processes custom zsgg components like: * - {{stats-table faction=grell type=unit}} * - {{ability-breakdown entity-id}} * - {{faction-tree faction=legion}} * - $$combat-sim unit1=stinger unit2=marine$$ */ const remarkCustomComponents = () => { return (tree) => { visit(tree, 'text', (node, index, parent) => { if (!parent || typeof index !== 'number') return; const text = node.value; // Component patterns: {{component args}} or $$component args$$ const componentPattern = /(\{\{|\$\$)([^}$]+)(\}\}|\$\$)/g; if (!componentPattern.test(text)) return; // For now, just mark these as code blocks to preserve them // Later we can implement actual component processing const parts = []; let lastIndex = 0; let match; componentPattern.lastIndex = 0; while ((match = componentPattern.exec(text)) !== null) { if (match.index > lastIndex) { parts.push({ type: 'text', value: text.slice(lastIndex, match.index) }); } // Mark as inline code for now parts.push({ type: 'inlineCode', value: `ZSGG-COMPONENT: ${match[2]}` }); lastIndex = match.index + match[0].length; } if (lastIndex < text.length) { parts.push({ type: 'text', value: text.slice(lastIndex) }); } if (parts.length > 1) { parent.children.splice(index, 1, ...parts); } }); }; }; /** * Main markdown processor with all zsgg enhancements */ export const zsggMarkdownProcessor = unified() .use(remarkParse) .use(remarkSmartHeaderShift, 3) // Shift headers to start at h3 .use(remarkEntityAutoLink) // Convert [[entity:id]] to links .use(remarkCustomComponents) // Process {{component}} syntax .use(remarkStringify, { bullet: '-', // Use - for bullet lists emphasis: '*', // Use * for emphasis strong: '*', // Use ** for strong listItemIndent: 'one', // Consistent list indentation fences: false, // Don't use fenced code blocks rule: '-' // Use - for horizontal rules }); /** * Process markdown text with zsgg enhancements * Replaces the old normalizeText function with proper AST-based processing */ /** * Normalize indentation from template literals before processing */ function normalizeIndentation(text) { // Split into lines const lines = text.split(/\r?\n/); // Find minimum indentation (ignoring empty and whitespace-only lines) const contentLines = lines.filter(line => line.trim().length > 0); if (contentLines.length === 0) { return ""; } const minIndent = Math.min(...contentLines.map(line => line.match(/^\s*/)?.[0].length ?? 0)); // Remove common indentation, preserving empty/whitespace-only lines as empty const normalizedLines = lines.map(line => { if (line.trim().length === 0) { // Keep empty or whitespace-only lines as completely empty return ''; } // Remove common indentation from content lines return line.slice(minIndent); }); return normalizedLines.join('\n'); } export function processZsggMarkdown(markdown) { if (!markdown) { return undefined; } try { // First normalize indentation from template literals const normalizedMarkdown = normalizeIndentation(markdown); // Then process with remark const result = zsggMarkdownProcessor.processSync(normalizedMarkdown); return result.toString().trim(); } catch (error) { // Fallback to normalized text if remark processing fails console.warn('Markdown processing failed, using normalized text:', error); return normalizeIndentation(markdown); } } /** * Legacy alias for backward compatibility * @deprecated Use processZsggMarkdown instead */ export const normalizeText = processZsggMarkdown; //# sourceMappingURL=markdown.js.map