How to avoid duplicate id attributes after copy/paste?

Brent Hartwig discusses the problem and shares code Last Updated: 2006-09-20

=Question by N.N.=

When I copy/paste a tag, the id attribute of the tag is not incremented. I want that it is incremented with one, but the tag I have pasted keeps the same id that the one I have copied.

=Brent Hartwig answers=

Be aware there are many ways tags can be inserted into a document and if you need unique IDs, adding 1 to the value won't cut it. That said, below is some code that illustrates some of the ways tags may be inserted and implements the "standard paste".

To use, copy the code to its own file and source. That action will set several of the document callbacks. Start atitrace.exe (same dir as epic.exe) as this is where the debug messages are sent. Open up a document (I used the axdocbook sample, specifically the "role" attribute on .). Copy some content that includes tags. Paste somewhere. A response panel should popup indicating the pasted content is selected. Once you dismiss this, all the attributes of each tag inserted are displayed in the atitrace window. Further, if any of the pasted elements supports the "role" attribute, the code will attempt to add 1 to its value. If it is a non-numeric value, you will get an error message. To configure for your dtd, search and replace "role" with your ID attribute's name (I didn't use axdocbook's id attr as it does not support numeric-only values.).

I've never really liked how one must accomplish this: catch multiple events where each event must be handled in its own way. In the case of the paste callback, wait for the second hit, perform the paste yourself, do what you needed to do all along then tell Epic don't paste as you took care of it. It works and I haven't tried to think of a better way; but (there's always a but), it's pretty easy to miss some events thus leaving holes in your implementation.

An alternative to all of this is to question when these elements must have their IDs updated? Can you rip thru the entire doc upon save, open, or some other event that uses the IDs? If you can answer yes to this, I think you'll find that would be an easier and more bulletproof approach.

This was written and tested with 5.1H.

package catchInsert; function eval( msg ) { msg = caller( 1 ). ": " . msg; eval msg output=*debug; } function evalAttrs( oid ) { eval( "Attributes presently set on $oid, <" . oid_name(oid) . \    "> (invalid attrs excluded):" ); local attrName; local attrs[] if ( oid_attr_list( oid, attrs ) ) { for( attrName in attrs ) { eval( "  $attrName set to \"" . attrs[attrName] . "\"" ); }  } else { eval( "  [no attributes are set on this node]" ); } } function insertContentCallback(doc, startoid, startpos, endoid, endpos) { eval( "doc: $doc, startoid: $startoid, startpos: $startpos, " . \    "endoid: $endoid, endpos: $endpos" ); } function insertTagCallback(doc, tagname, op) { eval( "doc: $doc, tagname: $tagname, op: $op" ); } function insertTagAfterCallback(doc, tagname, op) { eval( "doc: $doc, tagname: $tagname, op: $op" ); } function insertTagAutoCallback(doc, oids[], op) { eval( "doc: $doc, count(oids): " . count(oids) . ", op: $op" ); } function pasteCallback(doc, buffername, op) { eval( "doc: $doc, buffername: $buffername, op: $op" ); local rtn = 0; if ( op == 1 ) { eval( "OP1, oid_caret: " . oid_caret . \      ", oid_caret_offset(oid_caret): " . oid_caret_offset(oid_caret) ); } else { eval( "OP2 BEFORE PASTE, oid_caret: " . oid_caret . \      ", oid_caret_offset(oid_caret): " . oid_caret_offset(oid_caret) ); local start_o = oid_caret; local start_off = oid_caret_offset( start_o ); paste $buffername; local end_o = oid_caret; local end_off = oid_caret_offset( end_o ); local end_oid_first_off = oid_caret_offset( oid_first ); # used in loop. eval( "OP2 AFTER PASTE, oid_caret: " . oid_caret . \      ", oid_caret_offset(oid_caret): " . oid_caret_offset(oid_caret) ); # Don't let the user see us moving the caret. doc_update_display( doc, 0 ); # How to select the pasted content: # (not necessary. use for debugging.) goto_oid( start_o, start_off ); mark -selection begin; goto_oid( end_o, end_off ); mark -selection end; scroll_to_oid( start_o, start_off ); response( "Here is the pasted content." ); clear_mark; # How to access all tags: goto_oid( start_o, start_off ); # only find start tags. local find_cmd = 'find -f -e -noselect -t -nowrapscan -q "<[^/][^>]*>"'; local o;    local attrName = "role"; local attrValue; while( execute( find_cmd ) == 0 ) { # Primary means out of this loop - only process content we just pasted. if ( oid_caret_offset( oid_first ) > end_oid_first_off ) { break }; o = oid_current_tag; # don't use oid_caret # sample / debug only evalAttrs( o ); # If truly what you want, here's where you could add one to a particular # attribute's value. if ( tag_has_attr( oid_name( o ), attrName ) ) { attrValue = oid_attr( o, attrName ); if ( attrValue == "" ) { attrValue = 0 }; # good default? if ( match( attrValue, "[^0-9]" ) ) { response( "Unable to add 1 to a non-numeric value of " . \             "\"$attrValue\" (attr name: \"$attrName\")." ); } else { attrValue += 1; oid_modify_attr( o, attrName, attrValue ); }      }     }      # Restore caret location goto_oid( end_o, end_off ); doc_update_display( doc, 1 ); rtn = -1; }   return rtn; } function initPackage { set debug==extwin; # what about insert_entity, modify_tag and table callbacks? doc_add_callback( 0, "insert_content", \     "catchInsert::insertContentCallback" ); doc_add_callback( 0, "insert_tag", \     "catchInsert::insertTagCallback" ); doc_add_callback( 0, "insert_tag_after", \     "catchInsert::insertTagAfterCallback" ); doc_add_callback( 0, "insert_tag_auto", \     "catchInsert::insertTagAutoCallback" ); doc_add_callback( 0, "paste", \     "catchInsert::pasteCallback" ); } initPackage;