@@ -37,8 +37,34 @@ function getOperationsFromCnl(cnlText, mode = 'richgraph') {
3737 const { id : nodeId , payload : nodePayload } = processNodeHeading ( nodeBlock . heading ) ;
3838 operations . push ( { type : 'addNode' , payload : nodePayload , id : nodeId } ) ;
3939
40+ // Process main node content
4041 const neighborhoodOps = processNeighborhood ( nodeId , nodeBlock . content ) ;
4142 operations . push ( ...neighborhoodOps ) ;
43+
44+ // Process morphs
45+ for ( const morph of nodeBlock . morphs || [ ] ) {
46+ const morphId = `${ nodeId } _morph_${ morph . name . toLowerCase ( ) . replace ( / \s + / g, '_' ) } _${ Date . now ( ) } ` ;
47+
48+ // Add morph operation
49+ operations . push ( {
50+ type : 'addMorph' ,
51+ payload : {
52+ nodeId : nodeId ,
53+ morph : {
54+ morph_id : morphId ,
55+ node_id : nodeId ,
56+ name : morph . name ,
57+ relationNode_ids : [ ] ,
58+ attributeNode_ids : [ ]
59+ }
60+ } ,
61+ id : `${ nodeId } _morph_${ morph . name } `
62+ } ) ;
63+
64+ // Process morph content
65+ const morphOps = processMorphNeighborhood ( nodeId , morphId , morph . content ) ;
66+ operations . push ( ...morphOps ) ;
67+ }
4268 }
4369
4470 console . log ( `[CNL Debug] Generated operations:` , operations . map ( op => ( { type : op . type , id : op . id } ) ) ) ;
@@ -281,12 +307,28 @@ function buildStructuralTree(cnlText) {
281307
282308 for ( const line of lines ) {
283309 if ( ! line . trim ( ) ) continue ;
284- const headingMatch = line . match ( HEADING_REGEX ) || line . match ( SIMPLE_HEADING_REGEX ) ;
285- if ( headingMatch ) {
286- currentNodeBlock = { heading : line . trim ( ) , content : [ ] } ;
310+
311+ // Check if this is a main heading (#) or a morph heading (##)
312+ const mainHeadingMatch = line . match ( / ^ \s * ( # ) \s + ( .+ ) $ / ) ;
313+ const morphHeadingMatch = line . match ( / ^ \s * ( # # ) \s + ( .+ ) $ / ) ;
314+
315+ if ( mainHeadingMatch ) {
316+ // This is a main node heading
317+ currentNodeBlock = { heading : line . trim ( ) , content : [ ] , morphs : [ ] } ;
287318 tree . push ( currentNodeBlock ) ;
319+ } else if ( morphHeadingMatch && currentNodeBlock ) {
320+ // This is a morph definition - add it to the current node block
321+ const morphName = morphHeadingMatch [ 2 ] . trim ( ) ;
322+ currentNodeBlock . morphs . push ( { name : morphName , content : [ ] } ) ;
288323 } else if ( currentNodeBlock ) {
289- currentNodeBlock . content . push ( line ) ;
324+ // This is content - add it to the current context
325+ if ( currentNodeBlock . morphs . length > 0 ) {
326+ // Add to the last morph
327+ currentNodeBlock . morphs [ currentNodeBlock . morphs . length - 1 ] . content . push ( line ) ;
328+ } else {
329+ // Add to the main node content
330+ currentNodeBlock . content . push ( line ) ;
331+ }
290332 }
291333 }
292334 return tree ;
@@ -366,8 +408,6 @@ function processNeighborhood(nodeId, lines) {
366408 const neighborhoodOps = [ ] ;
367409 let content = lines . join ( '\n' ) ;
368410
369-
370-
371411 const descriptionMatch = content . match ( DESCRIPTION_REGEX ) ;
372412 if ( descriptionMatch ) {
373413 const description = descriptionMatch [ 1 ] . trim ( ) ;
@@ -492,14 +532,17 @@ function processNeighborhood(nodeId, lines) {
492532 id : targetId
493533 } ) ;
494534
535+ // Build relation payload
536+ const relationPayload = {
537+ source : nodeId ,
538+ target : targetId ,
539+ name : relationName . trim ( )
540+ } ;
541+
495542 // Create relation to the target node
496543 neighborhoodOps . push ( {
497544 type : 'addRelation' ,
498- payload : {
499- source : nodeId ,
500- target : targetId ,
501- name : relationName . trim ( )
502- } ,
545+ payload : relationPayload ,
503546 id
504547 } ) ;
505548 }
@@ -508,6 +551,152 @@ function processNeighborhood(nodeId, lines) {
508551 return neighborhoodOps ;
509552}
510553
554+ function processMorphNeighborhood ( nodeId , morphId , lines ) {
555+ const neighborhoodOps = [ ] ;
556+ let content = lines . join ( '\n' ) ;
557+
558+ const descriptionMatch = content . match ( DESCRIPTION_REGEX ) ;
559+ if ( descriptionMatch ) {
560+ const description = descriptionMatch [ 1 ] . trim ( ) ;
561+ const id = `attr_${ nodeId } _description_${ crypto . createHash ( 'sha1' ) . update ( description ) . digest ( 'hex' ) . slice ( 0 , 6 ) } ` ;
562+ neighborhoodOps . push ( { type : 'updateNode' , payload : { id : nodeId , fields : { description } } , id : `${ nodeId } _description` } ) ;
563+ content = content . replace ( DESCRIPTION_REGEX , '' ) . trim ( ) ;
564+ }
511565
566+ // Process attributes with priority on unit extraction
567+ const attributeLines = content . split ( '\n' ) . filter ( line => line . trim ( ) . startsWith ( 'has ' ) ) ;
568+
569+ for ( const line of attributeLines ) {
570+ // Simple regex to extract basic parts
571+ const basicMatch = line . match ( / ^ \s * h a s \s + ( [ ^ : ] + ) : \s * ( [ ^ ; ] + ) ; ? / ) ;
572+ if ( ! basicMatch ) continue ;
573+
574+ const [ , name , fullValue ] = basicMatch ;
575+ let value = fullValue . trim ( ) ;
576+ let unit = null ;
577+ let adverb = null ;
578+ let modality = null ;
579+ let quantifier = null ;
580+
581+ // Priority 1: Extract units (*unit*)
582+ const unitMatch = value . match ( / \* ( [ ^ * ] + ) \* / ) ;
583+ if ( unitMatch ) {
584+ unit = unitMatch [ 1 ] . trim ( ) ;
585+ value = value . replace ( / \* [ ^ * ] + \* / , '' ) . trim ( ) ;
586+ }
587+
588+ // Priority 2: Extract quantifiers (*quantifier*)
589+ const quantifierMatch = value . match ( / \* ( [ ^ * ] + ) \* / ) ;
590+ if ( quantifierMatch ) {
591+ quantifier = quantifierMatch [ 1 ] . trim ( ) ;
592+ value = value . replace ( / \* [ ^ * ] + \* / , '' ) . trim ( ) ;
593+ }
594+
595+ // Priority 3: Extract adverbs (++adverb++)
596+ const adverbMatch = value . match ( / \+ \+ ( [ ^ + ] + ) \+ \+ / ) ;
597+ if ( adverbMatch ) {
598+ adverb = adverbMatch [ 1 ] . trim ( ) ;
599+ value = value . replace ( / \+ \+ [ ^ + ] + \+ \+ / , '' ) . trim ( ) ;
600+ }
601+
602+ // Priority 4: Extract modalities [modality]
603+ const modalityMatch = value . match ( / \[ ( [ ^ \] ] + ) \] / ) ;
604+ if ( modalityMatch ) {
605+ modality = modalityMatch [ 1 ] . trim ( ) ;
606+ value = value . replace ( / \[ [ ^ \] ] + \] / , '' ) . trim ( ) ;
607+ }
608+
609+ // Clean up the final value
610+ value = value . trim ( ) ;
611+
612+ console . log ( `[Morph Attribute Debug] Parsed:` , { name, value, unit, quantifier, adverb, modality } ) ;
613+
614+ const valueHash = crypto . createHash ( 'sha1' ) . update ( String ( value ) ) . digest ( 'hex' ) . slice ( 0 , 6 ) ;
615+ const id = `attr_${ nodeId } _${ name . trim ( ) . toLowerCase ( ) . replace ( / \s + / g, '_' ) } _${ valueHash } ` ;
616+
617+ // Build enhanced attribute payload with modifiers
618+ const attributePayload = {
619+ source : nodeId ,
620+ name : name . trim ( ) ,
621+ value : value ,
622+ morphId : morphId
623+ } ;
624+
625+ // Add modifiers in priority order
626+ if ( unit ) attributePayload . unit = unit ;
627+ if ( quantifier ) attributePayload . quantifier = quantifier ;
628+ if ( adverb ) attributePayload . adverb = adverb ;
629+ if ( modality ) attributePayload . modality = modality ;
630+
631+ neighborhoodOps . push ( { type : 'addAttribute' , payload : attributePayload , id } ) ;
632+ }
633+
634+ const functionMatches = [ ...content . matchAll ( FUNCTION_REGEX ) ] ;
635+
636+ for ( const match of functionMatches ) {
637+ const [ , name ] = match ;
638+
639+ const id = `func_${ nodeId } _${ name . trim ( ) . toLowerCase ( ) . replace ( / \s + / g, '_' ) } ` ;
640+ neighborhoodOps . push ( { type : 'applyFunction' , payload : { source : nodeId , name : name . trim ( ) } , id } ) ;
641+ }
642+
643+ const relationMatches = [ ...content . matchAll ( RELATION_REGEX ) ] ;
644+
645+ for ( const match of relationMatches ) {
646+ const [ , relationName , targets ] = match ;
647+
648+ for ( const target of targets . split ( ';' ) . map ( t => t . trim ( ) ) . filter ( Boolean ) ) {
649+ // Parse target for adjectives and base name (similar to node headings)
650+ let targetAdjective = null ;
651+ let targetBaseName = target ;
652+ let targetDisplayName = target ;
653+
654+ // Check if target has adjective formatting (*adjective* baseName)
655+ const adjectiveMatch = target . match ( / \* \* ? ( [ ^ * ] + ) \* \* ? \s + ( .+ ) / ) ;
656+ if ( adjectiveMatch ) {
657+ targetAdjective = adjectiveMatch [ 1 ] . trim ( ) ;
658+ targetBaseName = adjectiveMatch [ 2 ] . trim ( ) ;
659+ targetDisplayName = target ; // Keep the original formatting
660+ }
661+
662+ // Generate clean ID from base name and adjective if present
663+ const cleanTargetBaseName = targetBaseName . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 \s - ] / g, '' ) . replace ( / \s + / g, '_' ) ;
664+ const cleanTargetAdjective = targetAdjective ? targetAdjective . toLowerCase ( ) . replace ( / [ ^ a - z 0 - 9 \s - ] / g, '' ) . replace ( / \s + / g, '_' ) : null ;
665+ const targetId = cleanTargetAdjective ? `${ cleanTargetAdjective } _${ cleanTargetBaseName } ` : cleanTargetBaseName ;
666+ const id = `rel_${ nodeId } _${ relationName . trim ( ) . toLowerCase ( ) . replace ( / \s + / g, '_' ) } _${ targetId } ` ;
667+
668+ // Create target node if it doesn't exist (for implicit nodes like "Country", "Asia")
669+ neighborhoodOps . push ( {
670+ type : 'addNode' ,
671+ payload : {
672+ base_name : targetBaseName ,
673+ displayName : targetDisplayName ,
674+ role : 'class' , // Default role for implicit nodes
675+ options : {
676+ adjective : targetAdjective
677+ }
678+ } ,
679+ id : targetId
680+ } ) ;
681+
682+ // Build relation payload
683+ const relationPayload = {
684+ source : nodeId ,
685+ target : targetId ,
686+ name : relationName . trim ( ) ,
687+ morphId : morphId
688+ } ;
689+
690+ // Create relation to the target node
691+ neighborhoodOps . push ( {
692+ type : 'addRelation' ,
693+ payload : relationPayload ,
694+ id
695+ } ) ;
696+ }
697+ }
698+
699+ return neighborhoodOps ;
700+ }
512701
513702export { diffCnl , validateOperations , getNodeOrderFromCnl , getOperationsFromCnl } ;
0 commit comments