// Dragon Code V2.6 Parser function parseDragonCode(input) { const result = { species: null, tags: {}, errors: [] }; try { // Tokenize the input const tokens = tokenize(input); // Process each token tokens.forEach((token, index) => { try { const tagType = identifyTagType(token, index); if (tagType === 'species') { result.species = parseSpecies(token); } else if (tagType) { const parsed = parseTag(token, tagType); if (parsed) { result.tags[tagType] = parsed; } } } catch (error) { result.errors.push(`Error parsing token "${token}": ${error.message}`); } }); } catch (error) { result.errors.push(`Error tokenizing input: ${error.message}`); } return result; } // Tokenizer: splits Dragon Code into individual tokens function tokenize(input) { // Remove DC2. prefix if present let code = input.trim(); if (code.startsWith('DC2.')) { code = code.substring(4); } else if (code.startsWith('DC.')) { code = code.substring(3); } const tokens = []; let current = ''; let inQuotes = false; for (let i = 0; i < code.length; i++) { const char = code[i]; if (char === '"') { inQuotes = !inQuotes; current += char; } else if (char === ' ' && !inQuotes) { if (current.trim()) { tokens.push(current.trim()); } current = ''; } else { current += char; } } if (current.trim()) { tokens.push(current.trim()); } return tokens; } // Identifies the type of tag function identifyTagType(token, index) { // IMPORTANT: Check two-letter tags before single-letter tags to avoid conflicts // (e.g., Tc before T, Ac before A, Sk before S, Df before D) // Two-letter tags first if (token.startsWith('Tc')) return 'technology'; if (token.startsWith('Ac')) return 'activity'; if (token.startsWith('Sk')) return 'skinType'; if (token.startsWith('Df')) return 'dragonFriend'; if (token.startsWith('Ph') || token.startsWith('Pa') || token.startsWith('Pl') || token.startsWith('Pw') || token.startsWith('Pt') || token.startsWith('Pv') || token.startsWith('Pk') || token.startsWith('Pf') || token.startsWith('Pp')) return 'appendages'; // Species tag is typically first, or contains species indicators if (index === 0 || token.match(/^[~^]?[A-Z][a-z]*[\[+{^~]?/) || token.includes('/') || token.includes('[') || token.includes('^')) { // At index 0, check if it's a single-letter or multi-letter species code first // Species codes can be: D, Dw, H, A, Ag, etc. if (index === 0) { // At index 0, check in this order: // 1. Two-letter tag prefixes (Tc, Ac, Sk, Df, Ph, etc.) // 2. Single-letter tags (G, L, W, T, etc.) // 3. Species indicators (~, ^, [, +) - clear species markers // 4. Multi-letter species codes (Dw, De, etc.) // 5. Fallback to species for remaining patterns // Two-letter tags (must check BEFORE single-letter checks) if (token.startsWith('Tc')) return 'technology'; if (token.startsWith('Ac')) return 'activity'; if (token.startsWith('Sk')) return 'skinType'; if (token.startsWith('Df')) return 'dragonFriend'; if (token.startsWith('Ph') || token.startsWith('Pa') || token.startsWith('Pl') || token.startsWith('Pw') || token.startsWith('Pt') || token.startsWith('Pv') || token.startsWith('Pk') || token.startsWith('Pf') || token.startsWith('Pp')) return 'appendages'; // Special case: bare "H" at index 0 is ambiguous // "H" alone = Human (species), "H+++" = Hoard (tag) // Check if it's exactly "H" with no modifiers if (token === 'H') { return 'species'; // Treat bare "H" as Human species } // Single-letter tags (with or without modifiers) if (token.startsWith('G')) return 'gender'; if (token.startsWith('L')) return 'length'; if (token.startsWith('W')) return 'width'; if (token.startsWith('T')) return 'weight'; if (token.startsWith('C') && token[1] && token[1].match(/[a-z]/)) return 'color'; if (token.startsWith('B')) return 'breath'; if (token.startsWith('A')) return 'age'; if (token.startsWith('N')) return 'nativeLand'; if (token.startsWith('M')) return 'mating'; if (token.startsWith('O')) return 'offspring'; if (token.startsWith('H')) return 'hoard'; if (token.startsWith('$')) return 'money'; if (token.startsWith('F')) return 'diet'; if (token.startsWith('R')) return 'reality'; if (token.startsWith('J')) return 'humor'; if (token.startsWith('S')) return 'social'; if (token.startsWith('U')) return 'ubiquity'; if (token.startsWith('I')) return 'irritability'; if (token.startsWith('V')) return 'magic'; if (token.startsWith('Q')) return 'psyPower'; if (token.startsWith('E')) return 'emotion'; // Check for species-specific indicators (after tag checks) // ~ at start (shapeshifter), or [ ] (trapped), or + (cross) if (token.startsWith('~') || token.includes('[') || token.includes('+')) { return 'species'; } // ^ for shaped (H^Dw), but not if it's a tag with ^ modifier if (token.includes('^') && token.match(/[A-Z][a-z]*\^[A-Z]/)) { return 'species'; } // / for multiple species in shapeshifter (must have species codes on both sides) // E.g., "Dw/H" or within "~Dw/H", but NOT "O/" or "M/" if (token.includes('/') && token.match(/[A-Z][a-z]*\/[A-Z]/)) { return 'species'; } // Multi-letter species codes (Dw, De, Ag, etc.) if (token.match(/^[A-Z][a-z]+/)) { return 'species'; } } // Check if it's actually a non-species tag if (token.startsWith('G')) return 'gender'; if (token.startsWith('L')) return 'length'; if (token.startsWith('W')) return 'width'; if (token.startsWith('T') && !token.startsWith('Tc')) return 'weight'; if (token.startsWith('C') && (token.length > 1 && token[1].match(/[a-z]/))) return 'color'; if (token.startsWith('B')) return 'breath'; if (token.startsWith('A') && !token.startsWith('Ac')) return 'age'; if (token.startsWith('N')) return 'nativeLand'; if (token.startsWith('M')) return 'mating'; if (token.startsWith('O')) return 'offspring'; if (token.startsWith('H')) return 'hoard'; if (token.startsWith('$')) return 'money'; if (token.startsWith('F')) return 'diet'; if (token.startsWith('R')) return 'reality'; if (token.startsWith('J')) return 'humor'; if (token.startsWith('S') && !token.startsWith('Sk')) return 'social'; if (token.startsWith('U')) return 'ubiquity'; if (token.startsWith('I')) return 'irritability'; if (token.startsWith('V')) return 'magic'; if (token.startsWith('Q')) return 'psyPower'; if (token.startsWith('E')) return 'emotion'; // If none of the above, likely species if (index === 0) return 'species'; } // Single-letter tags (checked after two-letter tags) // Gender if (token.startsWith('G')) return 'gender'; // Length (with or without numbers) if (token.startsWith('L')) return 'length'; // Width (with any modifiers) if (token.startsWith('W')) return 'width'; // Weight (Tonnage) - already checked it's not Technology if (token.startsWith('T')) return 'weight'; // Color if (token.startsWith('C')) return 'color'; // Breath Weapon if (token.startsWith('B')) return 'breath'; // Age - already checked it's not Activity if (token.startsWith('A')) return 'age'; // Native Land if (token.startsWith('N')) return 'nativeLand'; // Mating if (token.startsWith('M')) return 'mating'; // Offspring if (token.startsWith('O')) return 'offspring'; // Hoard if (token.startsWith('H')) return 'hoard'; // Money if (token.startsWith('$')) return 'money'; // Diet if (token.startsWith('F')) return 'diet'; // Reality if (token.startsWith('R')) return 'reality'; // Humor if (token.startsWith('J')) return 'humor'; // Social - already checked it's not Skin Type if (token.startsWith('S')) return 'social'; // Ubiquity if (token.startsWith('U')) return 'ubiquity'; // Irritability if (token.startsWith('I')) return 'irritability'; // Magic if (token.startsWith('V')) return 'magic'; // Psy Power if (token.startsWith('Q')) return 'psyPower'; // Emotion if (token.startsWith('E')) return 'emotion'; return null; } // Parse species (handles ~, ^, [], +, {}) function parseSpecies(token) { const result = { type: 'simple', value: null, modifiers: [], raw: token }; // Handle quoted custom species if (token.startsWith('"') && token.endsWith('"')) { result.type = 'custom'; result.value = token.substring(1, token.length - 1); return result; } // Shapechanger: ~species1/species2 if (token.includes('~') && token.includes('/')) { result.type = 'shapechanger'; const parts = token.split('/'); result.forms = parts.map(p => p.replace('~', '').trim()); // Check for {} modifiers result.forms = result.forms.map(form => { if (form.includes('{')) { const match = form.match(/([^{]+)\{([^}]+)\}/); if (match) { return { species: match[1], modifier: match[2] }; } } return { species: form, modifier: null }; }); return result; } // Cross-breed: species1+species2 if (token.includes('+') && !token.match(/^[+\-]/)) { result.type = 'cross'; result.species = token.split('+').map(s => s.trim()); return result; } // Trapped form: species1[species2] if (token.includes('[') && token.includes(']')) { result.type = 'trapped'; const match = token.match(/([^\[]+)\[([^\]]+)\]/); if (match) { result.trueForm = match[1]; result.trappedIn = match[2]; } return result; } // Shaped: species1^species2 if (token.includes('^')) { result.type = 'shaped'; const parts = token.split('^'); result.trueForm = parts[0]; result.shapedAs = parts[1]; return result; } // Simple species code result.value = token; return result; } // Parse individual tag based on type function parseTag(token, type) { const parsers = { gender: parseGender, length: parseLength, width: parseSimpleModifier, weight: parseSimpleModifier, appendages: parseAppendages, skinType: parseSkinType, color: parseColor, breath: parseBreath, age: parseSimpleModifier, nativeLand: parseSimpleModifier, mating: parseMating, offspring: parseOffspring, hoard: parseSimpleModifier, money: parseSimpleModifier, diet: parseDiet, reality: parseSimpleModifier, activity: parseSimpleModifier, humor: parseSimpleModifier, social: parseSimpleModifier, ubiquity: parseSimpleModifier, irritability: parseSimpleModifier, magic: parseSimpleModifier, psyPower: parseSimpleModifier, technology: parseTechnology, emotion: parseSimpleModifier, dragonFriend: parseSimpleModifier }; const parser = parsers[type]; if (parser) { return parser(token); } return { raw: token }; } // Parse gender function parseGender(token) { // Handle quoted custom gender if (token.includes('"')) { const match = token.match(/"([^"]+)"/); return { type: 'custom', value: match ? match[1] : token, raw: token }; } return { value: token.substring(1), // Remove 'G' prefix modifiers: extractModifiers(token.substring(1)), raw: token }; } // Parse length (quantitative with units) function parseLength(token) { const result = { raw: token, value: null, unit: null, dimensions: [], // For quantitative modifiers like 4t (4m tail) modifiers: null // For qualitative modifiers like +++, -, etc. }; // Check if it has a numeric value (quantitative) const match = token.match(/L(\d+)([a-z]+)?/i); if (match) { result.value = parseInt(match[1]); result.unit = match[2] || null; // Extract dimension modifiers (a, l, n, t, w, h) const dimMatch = token.match(/\d+[a-z]*(\d*[alntwh])/); if (dimMatch) { result.dimensions = dimMatch[1].split('').map(m => { if (m.match(/\d/)) return null; return m; }).filter(Boolean); } } else { // Qualitative length (L+++, L-, L, etc.) // Remove 'L' prefix and extract modifiers const modifierPart = token.substring(1); result.modifiers = extractModifiers(modifierPart); } return result; } // Parse simple modifier tags (+, -, !, ?, ~, etc.) function parseSimpleModifier(token) { // Remove tag prefix to get modifiers let modifierPart = token.replace(/^[A-Z][a-z]?/, ''); return { modifiers: extractModifiers(modifierPart), raw: token }; } // Extract modifiers from a string function extractModifiers(str) { const modifiers = { plus: 0, minus: 0, exclaim: false, question: false, tilde: false, caret: false, slash: false, real: false, virtual: false }; for (let char of str) { if (char === '+') modifiers.plus++; else if (char === '-') modifiers.minus++; else if (char === '!') modifiers.exclaim = true; else if (char === '?') modifiers.question = true; else if (char === '~') modifiers.tilde = true; else if (char === '^') modifiers.caret = true; else if (char === '/') modifiers.slash = true; } // Check for r/v (real/virtual) if (str.includes('r')) modifiers.real = true; if (str.includes('v')) modifiers.virtual = true; return modifiers; } // Parse appendages (complex sequence) function parseAppendages(token) { const result = { parts: [], raw: token }; // Parse the appendage code let current = ''; for (let i = 0; i < token.length; i++) { const char = token[i]; if (char === 'P') continue; // Skip prefix if (char.match(/[halvwtkfp']/)) { if (current) { result.parts.push(parseAppendagePart(current)); } current = char; } else { current += char; } } if (current) { result.parts.push(parseAppendagePart(current)); } return result; } function parseAppendagePart(part) { const type = part[0]; const modifiers = part.substring(1); return { type: type, modifiers: extractModifiers(modifiers), count: modifiers.match(/\d+/) ? parseInt(modifiers.match(/\d+/)[0]) : null }; } // Parse skin type function parseSkinType(token) { const result = { mainType: null, bodyPartTypes: [], // Array of {part, type} objects modifiers: {}, raw: token }; // Remove Sk prefix let content = token.substring(2); // Check for ? modifier if (content.includes('?')) { result.modifiers.question = true; content = content.replace('?', ''); } // Split by comma to separate main type from body part modifiers const parts = content.split(','); // First part is the main skin type (single letter) if (parts[0]) { result.mainType = parts[0][0]; // First character is the main type } // Remaining parts are body part modifiers (format: ) for (let i = 1; i < parts.length; i++) { const part = parts[i]; if (part.length >= 2) { // First character is body part, rest is skin type const bodyPart = part[0]; const skinType = part.substring(1); result.bodyPartTypes.push({ part: bodyPart, type: skinType }); } } return result; } // Parse color (MOST COMPLEX) function parseColor(token) { const result = { colors: [], raw: token }; // Remove C prefix let content = token.substring(1); // Split by / for multiple colors, and handle \ as additional color/effect const colorParts = content.split(/[\/\\]/); colorParts.forEach(colorPart => { if (colorPart) { result.colors.push(parseColorPart(colorPart)); } }); return result; } function parseColorPart(part) { const color = { base: null, intensity: [], patterns: [], bodyParts: [], modifiers: [] }; // Extract base color (2-3 letter code) const baseMatch = part.match(/^([a-z]{2,3})/); if (baseMatch) { color.base = baseMatch[1]; part = part.substring(baseMatch[1].length); } // Check for 'lum' (luminescent) special modifier if (part.includes('lum')) { color.intensity.push('lum'); part = part.replace('lum', ''); } // Extract intensity modifiers (+, -, ^, _, ', %, !) for (let char of part) { if (['+', '-', '^', '_', "'", '%', '!'].includes(char)) { color.intensity.push(char); } else if (['|', '=', ':', '*', '@', '#', '&', '>'].includes(char)) { color.patterns.push(char); } else if (char === ',') { // Body part modifier follows break; } } // Extract body part modifiers (,a, ,b, etc.) const bodyPartMatches = part.match(/,([a-z])/g); if (bodyPartMatches) { color.bodyParts = bodyPartMatches.map(m => m.substring(1)); } // Check for special patterns like &1, &2 const specialPattern = part.match(/&(\d+)/); if (specialPattern) { color.patterns.push('&' + specialPattern[1]); } return color; } // Parse breath weapon function parseBreath(token) { // Handle quoted custom breath if (token.includes('"')) { const match = token.match(/"([^"]+)"/); return { type: 'custom', value: match ? match[1] : token, raw: token }; } const result = { types: [], modifiers: [], raw: token }; // Remove B prefix let content = token.substring(1); // Check for modifiers if (content.includes('|')) result.modifiers.push('beam'); if (content.includes('#')) result.modifiers.push('cloud'); // Handle simple +/- modifiers (no specific type) if (content.match(/^[+\-!?~]+$/)) { result.simple = extractModifiers(content); return result; } // Extract breath types (fl, ac, ic, etc.) if (content.length > 0) { const types = content.split('/').map(t => t.replace(/[|#]/g, '').trim()).filter(Boolean); result.types = types; } else { // "B" with no content - normal breath result.simple = extractModifiers(''); } return result; } // Parse mating function parseMating(token) { const result = { modifiers: extractModifiers(token.substring(1)), count: null, separations: null, raw: token }; // Extract count (number at end) const countMatch = token.match(/(\d+)$/); if (countMatch) { result.count = parseInt(countMatch[1]); } // Extract separations (^number) const sepMatch = token.match(/\^(\d+)/); if (sepMatch) { result.separations = parseInt(sepMatch[1]); } return result; } // Parse offspring function parseOffspring(token) { const result = { modifiers: extractModifiers(token.substring(1)), count: null, adopted: false, raw: token }; // Check for adopted (a modifier) if (token.includes('a')) { result.adopted = true; } // Extract count const countMatch = token.match(/(\d+)/); if (countMatch) { result.count = parseInt(countMatch[1]); } return result; } // Parse diet function parseDiet(token) { const result = { modifiers: extractModifiers(token.substring(1)), types: [], raw: token }; // Extract diet type letters (c, h, o, v) const content = token.substring(1); for (let char of content) { if (['c', 'h', 'o', 'v'].includes(char)) { result.types.push(char); } } return result; } // Parse technology function parseTechnology(token) { const result = { modifiers: extractModifiers(token.substring(2)), specialist: null, raw: token }; // Extract specialist field in [] const specialistMatch = token.match(/\[([^\]]+)\]/); if (specialistMatch) { result.specialist = specialistMatch[1]; } return result; }