SAP ABAP – Generate HTML

Collection of classes and demo program to generate HTML code using ABAP. Generated HTML can be used to compose a well-formatted email with table and images.

Introduction

ZCL_HTML_ELEMENT is base class. Rest of the classes inherits from ZCL_HTML_ELEMENT. Method to_html of the class returns the HTML code including all nodes under the hierarchy. I have included source code of all classes later in the blog for you to copy if you decide to use these.

I have recently used these classes while sending email notifications from Purchase Order release workflow. Emails sent were well formatted with header and line item details rendered using table tag.

How to Use

1. Paragraph

To add paragraph to HTML document use class ZCL_HTML_PARAGRAPH. In its simplest form, you can create an instance of ZCL_HTML_PARAGRAPH passing text in importing parameter iv_value.

  data(html) = new zcl_html_document( ) .
  data(simple_paragraph) = new zcl_html_paragraph( 
                             iv_value = 'Simple text' ) .
  html->body->add_child( simple_paragraph )  .

  cl_demo_output=>write_html( html->to_html( ) ) .
  cl_demo_output=>display( ) .

To include long text, for example, Purchase order header text use method set_long_text specifying usual information to identify long text.

  data(html_paragraph) = new zcl_html_paragraph( ) .
  html_paragraph->set_long_text(
    exporting
      iv_id     = 'F02'
      iv_name   = po_header-ebeln
      iv_object = 'EKKO'  ).
  html->body->add_child( html_paragraph )  .

2. Structure

To add values from a structure in HTML document use class ZCL_HTML_TABLE calling method set_structure_data passing structure variable. You can optionally pass comma-separated list of fields to be included in HTML in parameter iv_visible_column.

select single from ekko fields * 
where ebeln = '4500000014' into @data(po_header) .

data(html_po_head) = new zcl_html_table( ).
html_po_head->set_struct_data(
                  is_structure = po_header
                  iv_visible_fields = 'ebeln,lifnr' ) .
html->body->add_child( html_po_head ) .

Class picks field header from dictionary so make sure you have a structure with fields defined with reference to the dictionary. Note that fields headers are rendered in the first column. If you prefer field header to be rendered as the first row then refer next section.

3. Table with Single line

In case you have structure which you like to display as below screen shot then create internal table of the structure and use method SET_TABLE_DATA of class ZCL_HTML_TABLE.

select from ekko fields * where ebeln = '4500000014' 
into table @data(po_head_line) .

data(table1) = new zcl_html_table( ).
table1->set_table_data(
              it_table = po_head_line
              iv_visible_columns = 'ebeln,lifnr' ) .
html->body->add_child( table1 )  .

4. Table with Multiple lines

select from ekpo fields * 
where ebeln = '4500000014' into table @data(polines) .

data(table) = new zcl_html_table( ).
table->set_table_data(
         it_table = polines
         iv_visible_columns = 'ebeln,ebelp,txz01,menge,meins' ) .
html->body->add_child( table ) .

5. Image with Link

Use class ZCL_HTML_IMAGE to include image in HTML. To include an image which refers to url, pass image url which creating instance of class ZCL_HTML_IMAGE in parameter iv_src.

data(img) = new zcl_html_image( 
                  iv_src = 'https://www.seekpng.com/png/full/382-3821698_hbo-logo-white-png-download-sap-logo-white.png'
                  iv_alt = 'SAP Logo' ).
img->set_width( '150' ).
html->body->add_child( img ) .

The HTML email with an image using URL reference are not downloaded automatically in MS Outlook. User will have to right-click on the image (or on email header) to download image(s). However, this results in email content with a smaller size.

There is however another (next section) way to embed image in HTML itself which will load the image without having the user to manually download it. This will though increase the email size as the image source is included as part of HTML.

6. Image with embedded content

To include mime image as part of html use method EMBEDD_MIME.

data(img_mime) = new zcl_html_image( iv_alt = 'mime embedded image' ) .
img_mime->embedd_mime( 
     iv_mime_path = '/SAP/PUBLIC/BOBF/BOB/bob_default_picture.png' ) .
img_mime->set_width( '150' ).

HTML generated will have image data inline rendering images in MS Outlook without having to download it manually.

Code

Class ZCL_HTML_ELEMENT

Base class ZCL_HTML_ELEMENT model the HTML element. Constructor let you create an element specifying tag, id, class and value. Additional attributes can be added using method add_attribute. Method to_html creates string by combining all variables and returns HTML string. The method to_html also include html for any child instance of HTML element added using method add_child.

html sap abap code class
class zcl_html_element definition
  public
  create public .

  public section.

    types:
      tt_html type standard table of ref to zcl_html_element .

    types: begin of ty_component,
             name type string,
             type type ref to cl_abap_typedescr,
           end of ty_component.

    types: tt_component type table of ty_component.

    data id type string read-only .
    data tag type string .
    data attributes type /iwbep/t_mgw_name_value_pair read-only .

    methods constructor
      importing
        !iv_tag   type string
        !iv_id    type string optional
        !iv_value type string optional
        !iv_class type string optional .
    methods to_html
      returning
        value(rv_value) type string .
    methods has_child
      returning
        value(rv_value) type boolean .
    methods add_child
      importing
        !html_element type ref to zcl_html_element .
    methods add_attribute
      importing
        !iv_name  type string
        !iv_value type string .
    methods get_value
      returning
        value(rv_value) type string .
    methods set_value
      importing
        !iv_value type string .
  protected section.
  private section.

    data children type tt_html .
    data value type string .
endclass.


class zcl_html_element implementation.

  method add_attribute.
    append value #( name = iv_name value = iv_value ) to attributes .
  endmethod.

  method add_child.
    append html_element to children .
  endmethod.

  method constructor.
    tag = iv_tag .
    value = iv_value .

    if iv_id is not initial .
      append value #( name = 'id' value = iv_id ) to attributes .
    endif.

    if iv_class is not initial .
      append value #( name = 'class' value = iv_class ) to attributes .
    endif.

  endmethod.

  method get_value.
    rv_value = value .
  endmethod.

  method has_child.
    if children is not initial .
      rv_value = abap_true .
    endif.
  endmethod.

  method set_value.
    value = iv_value .
  endmethod.

  method to_html.

    rv_value = |<{ tag }| < &&
                 reduce string( init att_html type string
                 for <attrib> in me->attributes
                 next att_html = att_html &&
                 | { <attrib>-name }="{ <attrib>-value }"| ) &&
               |>| &&
               value &&
                 reduce string( init child_html type string
                 for <child> in me->children
                 next child_html = child_html && <child>->to_html( ) ) &&
               |</{ tag }>| && |\n| .

  endmethod.
endclass.

Class ZCL_HTML_DOCUMENT

Class ZCL_HTML_DOCUMENT inherits from ZCL_HTML_ELEMENT. It creates <html> tag and tags <head> and <body> creating a hierarchy. You should use instance of this class to build document by adding futher elements to property body.

Method line_break added which you can use to add line break in HTML document.

class zcl_html_document definition
  public
  inheriting from zcl_html_element
  final
  create public .
  public section.
    data head type ref to zcl_html_element .
    data body type ref to zcl_html_element .

    methods constructor
      importing
        !iv_page_title type string .
    methods line_break .
  protected section.
  private section.
endclass.

class zcl_html_document implementation.

  method constructor.
    super->constructor( exporting iv_tag = 'html' ) .
    head = new zcl_html_element( iv_tag = 'head' iv_value = iv_page_title ) .
    body = new  zcl_html_element( iv_tag = 'body' ) .

    me->add_child( head ) .
    me->add_child( body ) .
  endmethod.

  method line_break.
    body->add_child( new zcl_html_line_break( ) ) .
  endmethod.
endclass.

Class ZCL_HTML_IMAGE

Class let you add image tag in HTML document specifying the URL of the image. Method embedd_mime let you add image from mime repository by embedding image data in HTML document itself.

class zcl_html_image definition
  public
  inheriting from zcl_html_element
  final
  create public .
  public section.
    methods constructor
      importing
        !iv_id    type string optional
        !iv_src   type string optional
        !iv_class type string optional
        !iv_alt   type string optional .
    methods set_width
      importing
        !iv_value type csequence .
    methods set_height
      importing
        !iv_value type csequence .
    methods embedd_mime
      importing
        !iv_mime_path type csequence default '/SAP/PUBLIC/BOBF/BOB/BOB_DEFAULT_PICTURE.PNG' . "/SAP/PUBLIC/BOBF/BOB/BOB_DEFAULT_PICTURE.PNG" .

    methods to_html
        redefinition .
  protected section.
  private section.
endclass.

class zcl_html_image implementation.
  method constructor.
    super->constructor(
      exporting
        iv_tag   = 'img'
        iv_id    = iv_id
        iv_class = iv_class ).

    if iv_src is not initial .
      append value #( name = 'src' value = iv_src ) to attributes .
    endif.

    if iv_alt is not initial .
      append value #( name = 'alt' value = iv_alt ) to attributes .
    endif.
  endmethod.

  method embedd_mime.
    data: lv_image_base64 type string .

    data(lo_mime_api) = cl_mime_repository_api=>get_api( ).

    lo_mime_api->get(
      exporting
        i_url                  = iv_mime_path
      importing
        e_content              = data(lv_content_x)
      exceptions
        parameter_missing      = 1
        error_occured          = 2
        not_found              = 3
        permission_failure     = 4        ).

    if sy-subrc <> 0.
      return .
    endif.

    call function 'SCMS_BASE64_ENCODE_STR'
      exporting
        input  = lv_content_x
      importing
        output = lv_image_base64.

    lv_image_base64 = 'data:image/png;base64,' && lv_image_base64 .

    read table attributes with key name = 'src' assigning field-symbol(<att>).
    if sy-subrc = 0 .
      <att>-value = lv_image_base64 .
    else.
      append value #( name = 'src' value = lv_image_base64 ) to attributes .
    endif.
  endmethod.

  method set_height.
    append value #( name = 'height' value = iv_value ) to attributes .
  endmethod.

  method set_width.
    append value #( name = 'width' value = iv_value ) to attributes .
  endmethod.

  method to_html.
    rv_value = |<{ tag }| &&
                 reduce string( init att_html type string
                 for <attrib> in me->attributes
                 next att_html = att_html &&
                 | { <attrib>-name }="{ <attrib>-value }"| ) &&
               |/>\n|.

  endmethod.
endclass.

Class ZCL_HTML_LINE_BREAK

Add line break tag <br>

class zcl_html_line_break definition
  public
  inheriting from zcl_html_element
  final
  create public .
  public section.
    methods constructor .
    methods to_html
        redefinition .
  protected section.
  private section.
endclass.

class zcl_html_line_break implementation.
  method constructor.
    super->constructor( exporting iv_tag = 'br' ).
  endmethod.
  method to_html.
    rv_value = |<{ tag }>\n| .
  endmethod.
endclass.

Class ZCL_HTML_PARAGRAPH

Add paragraph to html. Support to add long text as paragraph.

class zcl_html_paragraph definition
  public
  inheriting from zcl_html_element
  final
  create public .
  public section.
    methods constructor
      importing
        !iv_id    type string optional
        !iv_value type string optional
        !iv_class type string optional .
    methods set_long_text
      importing
        !iv_id     type csequence default 'ST'
        !iv_name   type csequence
        !iv_langu  type thead-tdspras default sy-langu
        !iv_object type csequence default 'TEXT' .
  protected section.
  private section.
    methods read_so10_to_str
      importing
        !iv_id         type thead-tdid default 'ST'
        !iv_name       type thead-tdname
        !iv_langu      type thead-tdspras default sy-langu
        !iv_object     type thead-tdobject default 'TEXT'
      returning
        value(rv_text) type string .
endclass.

class zcl_html_paragraph implementation.
  method constructor.
    super->constructor( exporting
                          iv_tag = 'p'
                          iv_id    = iv_id
                          iv_value = iv_value
                          iv_class = iv_class ).
  endmethod.

  method read_so10_to_str.

    data : lt_lines type idmx_di_t_tline .

    clear rv_text .

    call function 'READ_TEXT'
      exporting
        id                      = iv_id
        language                = iv_langu
        name                    = iv_name
        object                  = iv_object
      tables
        lines                   = lt_lines
      exceptions
        id                      = 1
        language                = 2
        name                    = 3
        not_found               = 4
        object                  = 5
        reference_check         = 6
        wrong_access_to_archive = 7.

    if sy-subrc <> 0 .
      return .
    endif.

    call function 'IDMX_DI_TLINE_INTO_STRING'
      exporting
        it_tline       = lt_lines
      importing
        ev_text_string = rv_text.

  endmethod.

  method set_long_text.
    data(lv_text) = read_so10_to_str(
                        exporting
                          iv_id     = conv #( iv_id )
                          iv_name   = conv #( iv_name )
                          iv_langu  = iv_langu
                          iv_object = conv #( iv_object ) ).
    set_value( iv_value = lv_text ) .
  endmethod.
endclass.

Class ZCL_HTML_TABLE

class zcl_html_table definition
  public
  inheriting from zcl_html_element
  final
  create public .
  public section.

    methods constructor
      importing
        !iv_id    type string optional
        !iv_class type string optional .
    methods set_table_data
      importing
        !it_table           type table
        !iv_visible_columns type csequence optional .
    methods set_struct_data
      importing
        !is_structure      type data
        !iv_visible_fields type csequence .
  protected section.
  private section.

    data mt_visible_column type stringtab .

    methods is_field_visible
      importing
        !iv_column           type string
      returning
        value(rv_is_visible) type abap_bool .
    methods get_text_by_rollname
      importing
        !iv_rollname   type csequence
      returning
        value(rv_text) type string .
    methods set_visible_fields
      importing
        !iv_visible_fields type csequence .
endclass.

class zcl_html_table implementation.
  method constructor.
    super->constructor( exporting iv_tag = 'table' iv_id = iv_id iv_class = iv_class ).
  endmethod.

  method get_text_by_rollname.
    select single scrtext_m
      from dd04t into rv_text
      where rollname = iv_rollname
      and ddlanguage = sy-langu.
  endmethod.

  method is_field_visible.
    if mt_visible_column is initial.
      rv_is_visible = abap_true.
    else.
      read table mt_visible_column transporting no fields with table key table_line = iv_column.
      if sy-subrc = 0.
        rv_is_visible = abap_true.
      else.
        rv_is_visible = abap_false.
      endif.
    endif.
  endmethod.

  method set_struct_data.
    data: lr_struct_desc type ref to cl_abap_structdescr,
          lt_comp        type abap_compdescr_tab,
          lt_comp_temp   type abap_compdescr_tab,
          lr_type_descr  type ref to cl_abap_typedescr,
          lt_component   type tt_component,
          lr_data        type ref to data,
          lv_label       type string,
          lv_rollname    type string,
          lr_table_descr type ref to cl_abap_tabledescr,
          ls_x030l       type x030l,
          lv_tabname     type tabname.

    field-symbols: <lt_table> type any table.

    set_visible_fields( iv_visible_fields = iv_visible_fields ).

    lr_struct_desc ?= cl_abap_typedescr=>describe_by_data( is_structure ).
    lt_comp = lr_struct_desc->components.
    loop at lt_comp assigning field-symbol(<ls_comp>).
      lr_type_descr = lr_struct_desc->get_component_type( p_name = <ls_comp>-name ).
      try.
          lr_struct_desc ?= lr_type_descr.
          lt_comp_temp = lr_struct_desc->components.
          delete lt_comp.
          append lines of lt_comp_temp to lt_comp.
        catch cx_sy_move_cast_error.
          append value #( name = <ls_comp>-name type = lr_type_descr ) to lt_component .
      endtry.
    endloop.

    assign is_structure to field-symbol(<ls_structure>).

    loop at lt_component assigning field-symbol(<ls_component>).

      if is_field_visible( <ls_component>-name ) = abap_false.
        continue.
      endif.

      if <ls_component>-type->is_ddic_type( ) = 'X'.
        lv_rollname = <ls_component>-type->get_relative_name( ).
        lv_label = get_text_by_rollname( lv_rollname ).
      else.
        lv_rollname = lv_label = <ls_component>-name.
      endif.

      data(row) = new zcl_html_element( iv_tag = 'tr' ) .
      me->add_child( row ) .
      row->add_child( new zcl_html_element( iv_tag = 'th' iv_value = lv_label ) ) .

      assign component <ls_component>-name of structure <ls_structure> to field-symbol(<lv_value>).
      try.
          lr_table_descr ?= <ls_component>-type.
          ls_x030l = lr_table_descr->get_ddic_header( ).
          lv_tabname = ls_x030l-tabname.
          clear: lv_label.
          create data lr_data like table of lv_tabname.
          assign lr_data->* to <lt_table>.
          <lt_table> = <lv_value>.
          loop at <lt_table> assigning <lv_value>.
            if lv_label <> ''.
              concatenate lv_label <lv_value> into lv_label separated by ','.
            else.
              lv_label = <lv_value>.
            endif.
          endloop.
        catch cx_sy_move_cast_error.
          if <lv_value> is assigned.
            lv_label = <lv_value>.
          endif.
      endtry.

      row->add_child( new zcl_html_element( iv_tag = 'td' iv_value = lv_label ) ) .
    endloop.

  endmethod.

  method set_table_data.

    data: lr_desc        type ref to cl_abap_tabledescr,
          lr_struct_desc type ref to cl_abap_structdescr,
          lt_comp_temp   type abap_compdescr_tab,
          lr_type_descr  type ref to cl_abap_typedescr,
          lt_component   type tt_component,
          lr_table_descr type ref to cl_abap_tabledescr,
          lv_label       type string.

    set_visible_fields( iv_visible_fields = iv_visible_columns ).

    lr_desc ?= cl_abap_typedescr=>describe_by_data( it_table ).
    lr_struct_desc ?= lr_desc->get_table_line_type( ).
    data(lt_comp) = lr_struct_desc->components.
    loop at lt_comp assigning field-symbol(<ls_comp>).
      lr_type_descr = lr_struct_desc->get_component_type( p_name = <ls_comp>-name ).
      try.
          lr_struct_desc ?= lr_type_descr.
          lt_comp_temp = lr_struct_desc->components.
          delete lt_comp.
          append lines of lt_comp_temp to lt_comp.
        catch cx_sy_move_cast_error.
          append value #( name = <ls_comp>-name type = lr_type_descr ) to lt_component .
      endtry.
    endloop.

    "table header
    loop at lt_component assigning field-symbol(<ls_component>).
      if is_field_visible( <ls_component>-name ) = abap_false.
        continue.
      endif.

      if <ls_component>-type->is_ddic_type( ) = abap_true .
        data(lv_rollname) = <ls_component>-type->get_relative_name( ).
        lv_label = get_text_by_rollname( lv_rollname ).
      else.
        lv_label = <ls_component>-name.
      endif.

      me->add_child( new zcl_html_element( iv_tag = 'th' iv_value = lv_label ) ) .
    endloop.

    loop at it_table assigning field-symbol(<ls_line>).
      "table rows
      data(row) = new zcl_html_element( iv_tag = 'tr' ) .
      me->add_child( row ) .

      loop at lt_component assigning <ls_component>.
        if is_field_visible( <ls_component>-name ) = abap_false.
          continue.
        endif.

        "table cell
        clear lv_label .
        try.
            lr_table_descr ?= <ls_component>-type.
          catch cx_sy_move_cast_error.
            assign component <ls_component>-name of structure <ls_line> to field-symbol(<lv_value>).
            if <lv_value> is assigned.
              lv_label = <lv_value>.
            endif.
        endtry.

        row->add_child( new zcl_html_element( iv_tag = 'td' iv_value = lv_label ) ) .
      endloop.
    endloop.
  endmethod.

  method set_visible_fields.

    if iv_visible_fields is not initial .
      data(lv_visible_columns) = iv_visible_fields .
      translate lv_visible_columns to upper case .
      split lv_visible_columns  at ',' into table mt_visible_column.
    endif.
  endmethod.
endclass.

Demo Program

report z_demo_html.

data html type ref to zcl_html_document .

*&---------------------------------------------------------------------*
*& start-of-selection
*&---------------------------------------------------------------------*
start-of-selection .

  select single from ekko fields * 
  where ebeln = '4500000014' into @data(po_header) .
  select from ekko fields * 
  where ebeln = '4500000014' into table @data(po_head_line) .
  select from ekpo fields * 
  where ebeln = '4500000014' into table @data(polines) .

  perform prepare_html .
  perform send_email.

*&---------------------------------------------------------------------*
*& send email
*&---------------------------------------------------------------------*
form send_email .

  try.
      data(send_request) = cl_bcs=>create_persistent( ).
      data(text_html) = cl_document_bcs=>string_to_soli( ip_string = html->to_html( ) ).
      data(document) = cl_document_bcs=>create_document(
                      i_type    = 'HTM'
                      i_text    = text_html
                      i_subject = 'Demo HTML Email' ).
      send_request->set_document( document ).

      data(recipient) = cl_cam_address_bcs=>create_internet_address(
                                        'replacewith@email.com' ).

      send_request->add_recipient(
                     i_recipient  = recipient
                     i_express    = abap_true ) .

      send_request->send( ).
      commit work.

    catch cx_bcs into data(bcs_exception).
  endtry .
endform.

*&---------------------------------------------------------------------*
*& PREPARE HTML
*&---------------------------------------------------------------------*
form prepare_html .

  html = new zcl_html_document( ) .

  data lv_css type string .
  lv_css =  | p \{                                              | &&
            |    font-family: Garamond, Helvetica, sans-serif;  | &&
            |    font-weight: bold;                             | &&
            |\}                                                 | &&
            |table \{                                           | &&
            |    border: 1px solid #000000;                     | &&
            |    border-collapse: collapse;                     | &&
            |    font-family: Garamond, Helvetica, sans-serif;  | &&
            |\}                                                 | &&
            |table td,                                          | &&
            |table th \{                                        | &&
            |    border: 1px solid #000000;                     | &&
            |    padding: 10px 10px;                            | &&
            |\}                                                 | &&
            |                                                   | &&
            |table tr:nth-child(even) \{                        | &&
            |    background: #D0E4F5;                           | &&
            |\}                                                 | &&
            |                                                   | &&
            |th \{                                              | &&
            |    background-color: #7EA8F8                      | &&
            |\}                                                 | .

  data(html_style) = new zcl_html_element( iv_tag = 'style' iv_value = lv_css ).
  html->head->add_child( html_style ) .

  data(html_paragraph) = new zcl_html_paragraph( ) .
  html_paragraph->set_long_text(
    exporting
      iv_id     = 'F02'
      iv_name   = po_header-ebeln
      iv_object = 'EKKO'  ).
  html->body->add_child( html_paragraph )  .

  data(html_po_head) = new zcl_html_table( ).
  html_po_head->set_struct_data(
                       is_structure = po_header
                       iv_visible_fields = 'ebeln,lifnr' ) .
  html->body->add_child( html_po_head )  .
  html->line_break( ) .

  data(table1) = new zcl_html_table( ).
  table1->set_table_data(
               it_table = po_head_line
               iv_visible_columns = 'ebeln,lifnr' ) .
  html->body->add_child( table1 )  .
  html->line_break( ) .

  data(table) = new zcl_html_table( ).
  table->set_table_data(
              it_table = polines
              iv_visible_columns = 'ebeln,ebelp,txz01,menge,meins' ) .
  html->body->add_child( table )  .
  html->line_break( ) .

  data(img) = new zcl_html_image( iv_src = 'https://www.seekpng.com/png/full/382-3821698_hbo-logo-white-png-download-sap-logo-white.png'
                                  iv_alt = 'SAP Logo' ).
  img->set_width( '150' ).
  html->body->add_child( img ) .
  html->line_break( ) .

  data(img_mime) = new zcl_html_image( iv_alt = 'mime embedded image' ) .
  img_mime->embedd_mime( iv_mime_path = '/SAP/PUBLIC/BOBF/BOB/bob_default_picture.png' ) .
  img_mime->set_width( '150' ).
  html->body->add_child( img_mime ) .

  "cl_demo_output=>write_html( html->to_html( ) ) .
  "cl_demo_output=>display( ) .
endform .

CSS

You would have noticed I have added css within STYLE tag to format the email. I think great way to format instead of formatting each element individually. You could ask your client for example to provide you with css while you focus on getting content for email.

With below css email appearance has improved.

        p {
            font-family: Garamond, Helvetica, sans-serif;
            font-weight: bold;
        }

        table {
            border: 1px solid #000000;
            border-collapse: collapse;
            font-family: Garamond, Helvetica, sans-serif;
        }

        table td,
        table th {
            border: 1px solid #000000;
            padding: 10px 10px;
        }

        table tr:nth-child(even) {
            background: #D0E4F5;
        }

        th {
            background-color: #7EA8F8;
        }

5 Replies to “SAP ABAP – Generate HTML

  1. to_html method has an error.
    rv_value = |<{ tag }| < &&
    "<" is not a permitted operator in this position of the calculation expression."

  2. Update to fix to_html:
    Simply remove the second ‘<' from rv_value = |<{ tag }| < &&
    i.e. it should be: rv_value = |<{ tag }| &&

Leave a Reply