@zerospacegg/iolin
Version:
Pure TypeScript implementation of ZeroSpace game data processing (PKL-free)
218 lines • 8.11 kB
JavaScript
/**
* 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