All files / web/src/components/toys/euclid euclidEntityMarkers.ts

97.75% Statements 87/89
82.5% Branches 33/40
100% Functions 3/3
97.75% Lines 87/89

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 901x 1x 1x 1x 50x 50x 50x 11x 50x 6x 50x 5x 50x 13x 50x 3x 50x 5x 50x 2x 50x 5x 50x   50x 50x 1x 1x 65x 65x 65x 15x 3x 65x 7x 1x 65x 6x 1x 65x 14x 1x 65x 4x 4x 4x 65x 7x 7x 7x 65x 2x 2x 2x 65x 10x 10x 10x 65x   65x 65x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 65x 65x 65x 65x 65x 65x 65x 65x 65x 1x  
import type { EntityMarkerConfig } from '@/lib/character/types'
import type { EuclidEntityRef } from './chat/parseGeometricEntities'
 
/** Display text for each entity tag */
function displayText(tag: string, value: string): string {
  switch (tag) {
    case 'seg':
      return value // "AB"
    case 'tri':
      return `\u25B3${value}` // "△ABC"
    case 'ang':
      return `\u2220${value}` // "∠ABC"
    case 'pt':
      return value // "A"
    case 'def':
      return `Definition ${value}`
    case 'post':
      return `Postulate ${value}`
    case 'cn':
      return `Common Notion ${value}`
    case 'prop':
      return `Proposition I.${value}`
    default:
      return value
  }
}
 
/** Build an EuclidEntityRef from tag + value, or null if invalid. */
function buildEntity(tag: string, value: string): EuclidEntityRef | null {
  switch (tag) {
    case 'seg':
      if (value.length === 2) return { type: 'segment', from: value[0], to: value[1] }
      return null
    case 'tri':
      if (value.length === 3) return { type: 'triangle', vertices: [value[0], value[1], value[2]] }
      return null
    case 'ang':
      if (value.length === 3) return { type: 'angle', points: [value[0], value[1], value[2]] }
      return null
    case 'pt':
      if (value.length === 1) return { type: 'point', label: value[0] }
      return null
    case 'def': {
      const n = parseInt(value, 10)
      return !isNaN(n) && n >= 1 ? { type: 'definition', id: n } : null
    }
    case 'post': {
      const n = parseInt(value, 10)
      return !isNaN(n) && n >= 1 ? { type: 'postulate', id: n } : null
    }
    case 'cn': {
      const n = parseInt(value, 10)
      return !isNaN(n) && n >= 1 ? { type: 'commonNotion', id: n } : null
    }
    case 'prop': {
      const n = parseInt(value, 10)
      return !isNaN(n) && n >= 1 ? { type: 'proposition', id: n } : null
    }
    default:
      return null
  }
}
 
/**
 * Unified entity marker config for Euclid chat.
 *
 * Matches geometric markers ({seg:AB}, {tri:ABC}, {ang:ABC}, {pt:A})
 * and foundation/proposition markers ({def:15}, {post:1}, {cn:1}, {prop:5}).
 *
 * Supports optional display text override: {prop:1|my first proposition}
 * renders as "my first proposition" instead of the canonical "Proposition I.1".
 *
 * The regex uses alternation: geometric tags require uppercase letters,
 * foundation tags require digits. Each branch has an optional |override group.
 */
export const EUCLID_ENTITY_MARKERS: EntityMarkerConfig<EuclidEntityRef> = {
  pattern:
    /\{(seg|tri|ang|pt):([A-Z]+)(?:\|([^}]*))?\}|\{(def|post|cn|prop):(\d+)(?:\|([^}]*))?\}/g,
  parseMatch: (groups) => {
    // Alternation: either groups[0]+[1]+[2?] matched (geometric) or groups[3]+[4]+[5?] (foundation)
    const tag = groups[0] ?? groups[3]
    const value = groups[1] ?? groups[4]
    const override = groups[2] ?? groups[5]
    if (!tag || !value) return null
    const entity = buildEntity(tag, value)
    if (!entity) return null
    return { entity, displayText: override || displayText(tag, value) }
  },
}