From f21615fbc5488cb07d43ed5285d6738ce0c02f28 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 10:23:51 +0000 Subject: [PATCH] ixml parser: a whitespace-only value is content, not formatting The parse loop skips the whitespace between two tags, which is right for the indentation of a pretty-printed document but wrong when that whitespace is everything an element contains: ` ` carries a blank and `{LF}` a line break, and both came back as an empty string. Through `CALL TRANSFORMATION id` and the parse back that is a lost value: a string that holds nothing but blanks or a line break is initial afterwards. The skip now keeps the whitespace when it stands directly between a start tag and its own end tag, and drops it everywhere else, so indentation is read as before - the first child of a pretty-printed element is still the element. Tests: the asXML roundtrip of a blank-only and of a newline-only value, the parser keeping the blank in ` `, and indentation still not turning into a #text node. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TKSWLpid4QLV6XWAoPixbb --- src/ixml/cl_ixml.clas.locals_imp.abap | 16 +++++- src/ixml/cl_ixml.clas.testclasses.abap | 31 +++++++++++ ..._call_transformation.clas.testclasses.abap | 51 +++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/src/ixml/cl_ixml.clas.locals_imp.abap b/src/ixml/cl_ixml.clas.locals_imp.abap index 03d8ee0b..202e63af 100644 --- a/src/ixml/cl_ixml.clas.locals_imp.abap +++ b/src/ixml/cl_ixml.clas.locals_imp.abap @@ -1303,6 +1303,7 @@ CLASS lcl_parser IMPLEMENTATION. DATA lv_xml TYPE string. DATA lv_rest TYPE string. DATA lv_whitespace TYPE string. + DATA lv_in_element TYPE abap_bool. DATA lv_bom TYPE c LENGTH 1. DATA lv_offset TYPE i. DATA lv_value TYPE string. @@ -1341,6 +1342,7 @@ CLASS lcl_parser IMPLEMENTATION. FIND FIRST OCCURRENCE OF '?>' IN lv_xml MATCH OFFSET lv_offset. ASSERT lv_offset > 0. lv_offset = lv_offset + 2. + lv_in_element = abap_false. ELSEIF lv_xml CP '<*'. * start or close tag FIND FIRST OCCURRENCE OF REGEX lc_regex_tag IN lv_xml RESULTS ls_match. @@ -1354,6 +1356,7 @@ CLASS lcl_parser IMPLEMENTATION. IF lv_xml CP 'if_ixml_node~get_parent( ). + lv_in_element = abap_false. ELSE. CREATE OBJECT lo_node EXPORTING ii_parent = lo_parent. IF lv_name CA ':'. @@ -1364,6 +1367,9 @@ CLASS lcl_parser IMPLEMENTATION. IF lv_tag NP '*/>'. lo_parent = lo_node. + lv_in_element = abap_true. + ELSE. + lv_in_element = abap_false. ENDIF. ENDIF. @@ -1384,6 +1390,7 @@ CLASS lcl_parser IMPLEMENTATION. CREATE OBJECT lo_node EXPORTING ii_parent = lo_parent. lo_node->if_ixml_node~set_name( '#text' ). lo_node->if_ixml_node~set_value( lcl_escape=>unescape_value( lv_value ) ). + lv_in_element = abap_false. ENDIF. lv_xml = lv_xml+lv_offset. @@ -1394,7 +1401,14 @@ CLASS lcl_parser IMPLEMENTATION. lv_rest = lv_xml. SHIFT lv_rest LEFT DELETING LEADING lv_whitespace. IF lv_rest IS INITIAL OR lv_rest(1) = '<'. - lv_xml = lv_rest. +* whitespace that stands between a start tag and its own end tag is the +* content of that element, ` ` carries a single blank, so it +* is left for the value branch below - everything else is formatting + IF lv_in_element = abap_true AND lv_rest CP ' lv_xml. + lv_in_element = abap_false. + ELSE. + lv_xml = lv_rest. + ENDIF. ENDIF. ENDWHILE. diff --git a/src/ixml/cl_ixml.clas.testclasses.abap b/src/ixml/cl_ixml.clas.testclasses.abap index d95cb4a0..f971856e 100644 --- a/src/ixml/cl_ixml.clas.testclasses.abap +++ b/src/ixml/cl_ixml.clas.testclasses.abap @@ -17,6 +17,8 @@ CLASS ltcl_xml DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATION SHORT FINAL. METHODS parse_basic FOR TESTING RAISING cx_static_check. METHODS root_element_after_crlf FOR TESTING RAISING cx_static_check. METHODS first_child_after_crlf FOR TESTING RAISING cx_static_check. + METHODS parse_blank_only_value FOR TESTING RAISING cx_static_check. + METHODS parse_indented_children FOR TESTING RAISING cx_static_check. METHODS parse_value_with_newline FOR TESTING RAISING cx_static_check. METHODS parse_bom FOR TESTING RAISING cx_static_check. METHODS parse_empty FOR TESTING RAISING cx_static_check. @@ -474,6 +476,35 @@ CLASS ltcl_xml IMPLEMENTATION. ENDMETHOD. + METHOD parse_blank_only_value. + + DATA li_root TYPE REF TO if_ixml_element. + + li_root = parse( | | )->get_root_element( ). + + cl_abap_unit_assert=>assert_equals( + act = li_root->get_first_child( )->get_value( ) + exp = ` ` ). + + ENDMETHOD. + + METHOD parse_indented_children. + + DATA li_root TYPE REF TO if_ixml_element. + DATA li_item TYPE REF TO if_ixml_node. + + " whitespace between two tags stays formatting, the first child of root + " is the element and not a #text node + li_root = parse( |\n A\n| )->get_root_element( ). + + li_item = li_root->get_first_child( ). + + cl_abap_unit_assert=>assert_equals( + act = li_item->get_name( ) + exp = `item` ). + + ENDMETHOD. + METHOD parse_bom. DATA lv_bom TYPE c LENGTH 1. diff --git a/src/kernel/call_transformation/kernel_call_transformation.clas.testclasses.abap b/src/kernel/call_transformation/kernel_call_transformation.clas.testclasses.abap index e6f9b0e5..bd697983 100644 --- a/src/kernel/call_transformation/kernel_call_transformation.clas.testclasses.abap +++ b/src/kernel/call_transformation/kernel_call_transformation.clas.testclasses.abap @@ -96,6 +96,8 @@ CLASS ltcl_call_transformation DEFINITION FOR TESTING RISK LEVEL HARMLESS DURATI METHODS escape_char_data FOR TESTING RAISING cx_static_check. METHODS escape_char_data_roundtrip FOR TESTING RAISING cx_static_check. METHODS escape_entity_text_roundtrip FOR TESTING RAISING cx_static_check. + METHODS blank_only_roundtrip FOR TESTING RAISING cx_static_check. + METHODS newline_only_roundtrip FOR TESTING RAISING cx_static_check. METHODS newline_roundtrip FOR TESTING RAISING cx_static_check. METHODS to_string_simple FOR TESTING RAISING cx_static_check. METHODS to_string_empty FOR TESTING RAISING cx_static_check. @@ -363,6 +365,55 @@ CLASS ltcl_call_transformation IMPLEMENTATION. exp = lv_exp ). ENDMETHOD. + METHOD blank_only_roundtrip. +* a value of one blank is content, not formatting + DATA lv_xml TYPE string. + DATA: BEGIN OF ls_data, + field TYPE string, + END OF ls_data. + + ls_data-field = ` `. + + CALL TRANSFORMATION id + SOURCE data = ls_data + RESULT XML lv_xml. + + CLEAR ls_data. + + CALL TRANSFORMATION id + SOURCE XML lv_xml + RESULT data = ls_data. + + cl_abap_unit_assert=>assert_equals( + act = ls_data-field + exp = ` ` ). + ENDMETHOD. + + METHOD newline_only_roundtrip. + DATA lv_xml TYPE string. + DATA lv_exp TYPE string. + DATA: BEGIN OF ls_data, + field TYPE string, + END OF ls_data. + + lv_exp = cl_abap_char_utilities=>newline. + ls_data-field = lv_exp. + + CALL TRANSFORMATION id + SOURCE data = ls_data + RESULT XML lv_xml. + + CLEAR ls_data. + + CALL TRANSFORMATION id + SOURCE XML lv_xml + RESULT data = ls_data. + + cl_abap_unit_assert=>assert_equals( + act = ls_data-field + exp = lv_exp ). + ENDMETHOD. + METHOD convert_json_to_sxml. DATA lo_writer TYPE REF TO cl_sxml_string_writer. lo_writer = cl_sxml_string_writer=>create( ).